缓存是 HTTP 最重要的性能手段,但规则也最容易写错:什么时候不用联网、什么时候要问服务器“变了没有”、什么时候缓存会把错的内容发给别人?这篇在真实服务器上看这些头部,用条件请求量出 304 省多少,再用一个最小缓存在 100 ms 往返的链路上比较四种服务器配置。
www.python.org 的 logo:200,15770 字节;Cache-Control = max-age=604800, public;ETag = "6acaec79-3d9a";Last-Modified = Sun, 11 Oct 2026 01:55:05 GMT;Date = Thu, 15 Oct 2026 07:50:31 GMT;Age = 249412 新鲜期:max-age 604800 秒 − Age 249412 秒(这份响应已经在 CDN 里待了这么久)= 还剩 355388 秒 ≈ 4.1 天;浏览器缓存里的这份副本再活这么久之内都不用联网 example.com 首页:200,577 字节;Cache-Control = None;ETag = None;Last-Modified = Tue, 13 Oct 2026 22:05:48 GMT;Age = 5 没有 Cache-Control、也没有 Expires,只有 Last-Modified:缓存可以用“启发式新鲜期”= 10% × (Date − Last-Modified) = 10% × 121484 秒 = 12148 秒 ≈ 3.4 小时(RFC 9111 建议的做法,各缓存实现不同)
max-age 减 Age 是剩余新鲜期;没有 Cache-Control 只有 Last-Modified 时缓存可以用 10% 启发式。
条件 状态 消息体 头部字节 响应总字节
(无条件) 200 15770 614 16384
If-None-Match: <当前 ETag> 304 0 364 364
If-None-Match: "不存在的 ETag" 200 15770 612 16382
If-Modified-Since: <Last-Modified> 304 0 365 365
If-Modified-Since: 一年前 200 15770 612 16382
两个都带(ETag 正确、日期过旧) 304 0 366 366
304 Not Modified 的响应只带头部(366 字节):它仍然带有 ['age', 'cache-control', 'date', 'etag'],缓存据此刷新副本的新鲜期;省下 16018 字节(97.8%)
example.com 用 If-Modified-Since: <它自己的 Last-Modified> → 304,消息体 0 字节(它没有 ETag,只能靠日期验证)If-None-Match / If-Modified-Since 匹配时返回 304,没有消息体;ETag 优先于日期。
配置 每次的结果 总网络字节 发出请求数 延迟(ms,每次)
无任何缓存头 200/200/200/200/200/200/200/200/200/200 500420 10 [101, 102, 102, 100, 102, 100, 100, 102, 100, 100]
max-age=60 200/hit/hit/hit/hit/hit/hit/hit/hit/hit 50069 1 [100, 0, 0, 0, 0, 0, 0, 0, 0, 0]
no-cache + ETag(每次都验证) 200/304/304/304/304/304/304/304/304/304 50673 10 [101, 101, 100, 101, 101, 100, 100, 102, 101, 101]
max-age=1 + ETag 200/hit/hit/304/hit/hit/304/hit/hit/304 50281 4 [102, 0, 0, 102, 0, 0, 101, 0, 0, 101]
“无任何缓存头”时客户端缓存没有可用的新鲜期,也没有验证器,每次都要重新下载整个 50 KB;max-age 内的访问完全不联网(延迟 0 ms);no-cache + ETag 每次都联网验证,但只付一个往返的延迟和很小的字节max-age 命中时 0 ms 且不联网;no-cache + ETag 每次付一个往返但只传几百字节;没有缓存头则每次完整下载。
只用 URL 做缓存键:甲(支持 gzip)拿到 gzip 压缩的 36 字节;乙(只收明文)拿到 gzip 压缩的 36 字节(乙收到了无法解读的压缩数据!);缓存里存了 1 份 URL + Vary: Accept-Encoding 做缓存键:甲(支持 gzip)拿到 gzip 压缩的 36 字节;乙(只收明文)拿到 明文 1200 字节;缓存里存了 2 份
缓存键漏掉影响内容的请求头,会把压缩数据发给不能解压的客户端。这一节是模拟。