Skip to content

入门

基于 Web 现代标准 · 核于 2026-07

速查

  • 多层缓存:浏览器发出的每个请求都会先过缓存层——Service Worker(Cache API)→ memory cache → disk cache → 网络;web.dev 原文:「浏览器发出的所有 HTTP 请求都会先被路由到浏览器缓存,检查是否有可用的有效缓存响应」。
  • disk cache 就是「HTTP 缓存」本体:跨会话持久、遵守 RFC 9111 的新鲜度语义(max-ageETag 那一套)。
  • memory cache:渲染进程内存里的短命高速层,同一标签页会话内复用(典型:同页重复出现的图片),关标签页即失效。
  • Service Worker + Cache API:开发者可编程的缓存层,不遵守 HTTP 缓存头、不自动过期,全靠代码做主。
  • bfcache(往返缓存)是另一维度:不缓存「响应」,而是把整页快照(DOM + JS 堆)冻结在内存,前进/后退瞬时恢复。
  • 「200 但没发请求」:强缓存命中时 DevTools Network 显示灰色 200,Size 栏写 (memory cache) / (disk cache) / (ServiceWorker)——请求根本没出网络。
  • 304 是「真请求」:协商缓存命中时确实发了条件请求,服务端只回状态行和头、不回响应体,本地副本被「续命」。
  • push cache 已从「四级缓存」里除名:HTTP/2 Server Push 已被 Chrome 106(2022)与 Firefox 132(2024)先后默认禁用。
  • 分工Cache-Control / ETag / 强协商语义在网络章;本叶讲浏览器拿到头之后的命中决策、缓存位置与清除
  • 观察三开关:Network 面板先开 Preserve log + Big request rows、确认 Disable cache 没勾,再谈判读。

一、一次请求要过几道缓存

在页面里写下一行 <img src="/logo.png">,这个请求从发起至拿到字节,中途有多次机会被「截住」:

顺序缓存层归谁管一句话职责
1Service Worker(Cache API)开发者代码若页面被 SW 控制,fetch 事件可拦截请求,用 caches.match() 直接回响应
2memory cache(内存缓存)浏览器(渲染进程)本标签页会话内刚用过的资源,直接从内存复用,快到近乎 0ms
3disk cache(磁盘缓存)浏览器(网络栈)即「HTTP 缓存」:按 Cache-Control 等语义判定新鲜/陈旧,跨会话持久
4网络以上全 miss(或需要协商验证)才真正出网

三点关键认知:

  1. 每层「归属」不同:Cache API 归你管(代码写什么就存什么),memory/disk cache 归浏览器管(你只能通过 HTTP 头影响它,不能编程读写)。
  2. 每层「寿命」不同:memory cache 随标签页关闭蒸发;disk cache 跨重启存活;Cache API 里的条目不删就一直在
  3. bfcache 不在这条链上:它作用于「整页导航」而非「单个资源请求」,是前进/后退专用的整页内存快照——和上表是两个维度的东西(详见 bfcache)。

老文章里的「四级缓存」还有一层 push cache(HTTP/2 Server Push 推来的资源暂存区)。这一层已随 Server Push 的移除成为历史——Chrome 106 起默认禁用,Firefox 132 跟进,理由与考古见多层缓存总览

二、「Network 显示 200 但没发请求」解剖

打开 DevTools Network,二次访问一个页面,常见三种「看起来都成功」的行:

现象StatusSize 栏真相
灰色 200200(memory cache)(disk cache)没发请求:强缓存命中,浏览器直接用本地副本回给页面
灰色 200200(ServiceWorker)没出网络:SW 的 fetch 事件用 Cache API 里的副本响应
正常 304304几百字节真发了请求:条件请求出网,服务端答「没变」,只传头不传体

为什么会有「假 200」?因为 HTTP 缓存的设计是对页面透明的:资源新鲜(fresh)时浏览器直接兑现本地副本,页面代码感知到的仍是一次成功响应,DevTools 便记一条 200,再用 Size 栏标注真实来源。判定链条是:

  1. 浏览器查缓存中是否有该 URL 的副本;
  2. 有,且按 max-age 等指令仍新鲜 → 直接用,不发请求(灰色 200);
  3. 有,但已陈旧(stale) → 自动带上 If-None-Match / If-Modified-Since条件请求——web.dev 原文:这些条件头「由浏览器根据它对 HTTP 缓存当前值的理解自动带上」;服务端没变则 304 续命,变了则 200 给新副本;
  4. 没有副本 → 正常出网。

判读口诀

Size 栏有括号 = 没花流量;Status 304 = 花了一个来回但没花响应体。 排查「用户拿到旧版」时,第一眼永远先看 Size 栏——它告诉你旧资源是从哪一层来的,才能决定去清哪一层。

观察前先拨好三个开关

  1. Preserve log:跨导航保留请求记录,不然一跳转证据就没了;
  2. Big request rows:Size 栏显示两行(传输量 / 资源真实大小),缓存效果一目了然;
  3. 确认 Disable cache 没勾着——勾着它你永远观察不到缓存命中(它只在 DevTools 打开期间生效,也常是「本地怎么都不命中」的元凶)。

高频三连问

  • 「缓存命中的 200,JS 还会执行吗?」 会。缓存兑现的是与当年网络响应字节相同的副本,解析、执行、渲染一切照旧——缓存只是省了「传输」,没省「使用」。
  • 「我都强制刷新了怎么还是旧的?」 强制刷新穿透的是 HTTP 缓存(memory/disk),穿不透 Service Worker——SW 的 fetch 事件照样拦截并回旧副本。看 Size 栏是不是 (ServiceWorker),是就去 SW 缓存那页找版本化收尸的答案。
  • 「304 算缓存命中吗?」 算「协商缓存」命中:请求真的出网了(花一个往返 + 头部字节),但响应体没传,本地副本被续命——它省的是带宽而非延迟。这也是为什么高频小资源与其依赖 304,不如给足 max-age 直接零请求。

三、二次访问的完整时间线

把上面串成一次真实的「昨天来过、今天再来」:

  1. HTML 入口(配了 no-cache):磁盘里有副本但每次必须协商 → 浏览器带 If-None-Match 出网 → 服务端答 304 → 用本地 HTML,只花了头部往返;
  2. 带哈希的 app.3f2a.js(配了 max-age=31536000, immutable):新鲜期内 → 不发请求,Size 栏 (disk cache)
  3. 页面里出现 6 次的 sprite.png:第一次 (disk cache),其余 5 次 (memory cache)——磁盘副本进内存后同文档复用;
  4. SW 控制的接口 GET /api/config:SW fetch 事件里 caches.match 命中 → Size 栏 (ServiceWorker),网络零参与;
  5. 用户点去第三方页又按后退回来:整页从 bfcache 解冻——以上四步一步都不用重来,连 JS 都不重新执行。

同一次「重访」,五种资源各命中一层。缓存排查的本领 = 能对每一行 Network 记录说出它命中的是哪层、为什么。

四、与网络章「HTTP 缓存首部」的分工

本库把 HTTP 缓存拆成了「协议语义」和「浏览器落地」两半,避免重复也避免断层:

问题属于哪半去哪读
max-age / no-cache / no-store / immutable 各是什么意思协议语义网络章 · 缓存首部
ETag 强弱验证器、304 的语义协议语义同上
新鲜/陈旧判定后浏览器怎么走、副本存内存还是磁盘浏览器落地本叶 HTTP 缓存的浏览器侧落地内存缓存与磁盘缓存
普通刷新和强制刷新有什么区别、各带什么头浏览器落地HTTP 缓存的浏览器侧落地
缓存怎么看、怎么清(DevTools / Clear-Site-Data / 用户清数据)浏览器落地观测与清除
HTTP/2 Server Push 的协议机制本身协议演进网络章 · HTTP 演进(本叶只讲 push cache 之死)

一句话:网络章管「头是什么意思」,本叶管「浏览器拿到头之后干了什么」。

五、入门期最常见的五个误区

误区纠正展开
no-cache = 不缓存」是「存但每次协商」;真不存是 no-store网络章
「让用户强制刷新就能拿到新版」强刷穿不透 SW 缓存,且没有用户会强刷;根治靠资源版本化HTTP 落地
「304 反正没传内容,等于免费」它仍花一个完整网络往返;高频资源应给足 max-age 做到零请求HTTP 落地
「SW 缓存是 HTTP 缓存的加强版」是完全另一套:不看 HTTP 头、不过期,忘清理即事故SW 缓存
「后退很快是因为缓存了 HTML」多半是 bfcache 恢复了整页快照,连 JS 执行都省了bfcache

六、本叶路线

多层缓存总览开始。