← 首页

XversePro 知识库 · 网络

HTTP 缓存:强缓存与协商缓存

浏览器先看缓存是否新鲜,过期了再拿 ETag 找服务器确认,没变就回 304。

结论:浏览器先问自己,再问服务器。缓存还新鲜就直接用,一个请求都不发,这就是常说的强缓存。过期了就带上 ETag 或修改时间去问服务器,没变就回 304,只省下响应体,这就是协商缓存。

强缓存和协商缓存是国内的叫法。RFC 9111 里对应的词是新鲜度(freshness)和验证(validation)。

1. 一张图看懂判断顺序

HTTP 缓存规则现在由 RFC 9111 定义,它在 2022 年 6 月发布,取代了旧的 RFC 7234。条件请求和 304 状态码则写在同时发布的 RFC 9110 里。

判断分两步。第一步看缓存里的响应是否新鲜,新鲜就直接用。第二步是缓存过期后,拿验证器去问服务器。验证器就是能判断内容变没变的凭据,常见的是 ETag 和 Last-Modified 两个响应头。

缓存里有这个 URL 的响应吗? 带 no-store 的响应从不入缓存 还新鲜吗? freshness_lifetime > current_age 有 ETag 或 Last-Modified 吗? no-cache 的响应每次都走到这里 发条件请求(协商缓存) If-None-Match / If-Modified-Since 发普通请求,拿 200 直接用缓存(强缓存) 不发任何网络请求 发普通请求,拿 200 304 Not Modified 继续用缓存,并更新头部 200 OK 内容变了,用新响应替换缓存 否是是否否是没变变了
图 1 缓存命中判断流程(示意,省略了 Vary 匹配和共享缓存的特殊规则)。

2. 新鲜期怎么算

缓存是否新鲜,只比较两个数:新鲜期有多长,响应已经存了多久。RFC 9111 第 4.2 节把它写成 response_is_fresh = (freshness_lifetime > current_age)。响应的年龄会参考 Age 响应头,它是发送方估算的秒数,从源服务器生成或验证这份响应时算起,见 RFC 9111 第 5.1 节。

新鲜期按下表从上往下找,用第一个能用的值。几个头同时出现时,max-age 优先于 Expires。

什么都没写时,缓存可以自己估一个值,叫启发式缓存。RFC 9111 第 4.2.2 节建议估值不超过距 Last-Modified 时间的一个比例,并举例说这个比例常取 10%。MDN 的缓存指南也建议所有响应都明确写 Cache-Control,不要依赖启发式缓存。

顺序来源新鲜期说明
1s-maxage=NN 秒只对共享缓存(如 CDN)生效
2max-age=NN 秒私有缓存和共享缓存都适用
3ExpiresExpires 减 Date用服务器时钟相减,减少两端时钟不准的影响
4都没有启发式估算例如距 Last-Modified 时长的 10%

表 1 新鲜期的取值顺序,依据 RFC 9111 第 4.2.1 节和第 4.2.2 节。

3. 过期后的协商:ETag 与 304

ETag 是服务器给某个版本内容打的标签,内容一变标签就变。浏览器下次请求时,把它放进 If-None-Match 请求头。服务器比对后发现没变,就回 304 Not Modified,不带响应体。

Last-Modified 是另一种验证器,记录内容的最后修改时间,配套请求头是 If-Modified-Since。两者同时出现时,RFC 9110 第 13.1.3 节要求服务器忽略 If-Modified-Since,因为 ETag 的判断更准确。

ETag 前面带 W/ 的是弱验证器,表示内容有小改动时标签可能不变,比如只精确到秒的时间戳生成的标签。RFC 9110 第 8.8.1 节说,弱验证器容易生成,但用于比较时远不如强验证器可靠。

304 不是空响应。RFC 9110 第 15.4.5 节要求它带上 200 响应本该有的 ETag、Cache-Control、Expires 等头,缓存据此刷新元数据,重新开始计算新鲜期。

浏览器服务器 GET /app.js 200 OK ETag: "v5" Cache-Control: max-age=60,带完整响应体 60 秒内再用:强缓存命中 不发请求 GET /app.js If-None-Match: "v5"(已过 60 秒) 304 Not Modified 没有响应体,浏览器继续用本地副本 如果 ETag 变了,这里会回 200 和新内容
图 2 一次强缓存命中和一次协商缓存(示意)。ETag 两边的双引号是语法的一部分。

4. 容易混的几个 Cache-Control 指令

最常被误读的是 no-cache。它不是不缓存,而是可以存,但每次使用前必须先找服务器验证,见 RFC 9111 第 5.2.2.4 节。真正不让存的是 no-store,见第 5.2.2.5 节。

private 和 public 讲的是谁可以存。private 只允许浏览器这类私有缓存保存,CDN 这类共享缓存不能存。public 明确表示可以缓存,即使响应在其他规则下原本不能被存。

指令能不能存用之前要不要验证典型场景
max-age=N能N 秒内不用普通静态资源
no-cache能每次都要HTML 入口页
no-store不能无缓存可验证含隐私数据的响应
private只能存在私有缓存看其他指令带用户信息的页面
public能,含共享缓存看其他指令希望 CDN 缓存的资源

表 2 常用响应指令,依据 RFC 9111 第 5.2.2 节和 MDN Cache-Control 文档。

5. 实用配方:入口页协商,静态资源长缓存

JS 和 CSS 常常更新。直接给 bundle.js 设很长的 max-age,用户可能一直拿旧文件。MDN 的缓存指南给出的做法是把版本号或内容哈希放进 URL,内容一变 URL 就变,旧缓存自然用不上。

这样静态资源可以设一年的 max-age,也就是 31536000 秒,再加 immutable 告诉浏览器刷新页面时也不必验证。HTML 入口页用 no-cache,每次都协商,保证能拿到新的资源地址。

index.html Cache-Control: no-cache 每次都协商 app.3f9a1c.js max-age=31536000, immutable style.8b2e07.css max-age=31536000, immutable 内容一变,文件名里的哈希就变,入口页引用新地址
图 3 入口页加带哈希的静态资源(示意,文件名中的哈希是随手写的例子)。

参考资料

  1. 《RFC 9111: HTTP Caching》(2022 年 6 月) https://www.rfc-editor.org/rfc/rfc9111.html
  2. 《RFC 9110: HTTP Semantics》(2022 年 6 月) https://www.rfc-editor.org/rfc/rfc9110.html
  3. 《HTTP caching》,MDN https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching
  4. 《Cache-Control》,MDN https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control
  5. 《ETag》,MDN https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag
  6. 《304 Not Modified》,MDN https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/304