缓存是引入静默正确性 bug 最容易的地方,因为缓存看起来一直正确,直到它提供了与源不一致的字节。能把这个心智模型维持清楚的,是把每个缓存都视为解码两只独立的钟,而不是一个过期值。
条件请求携带两类信息,把它们混为一谈就是陈旧内容上线的方式。一只是新鲜度——一个响应可以不询问就重用多久。另一只是校验——客户端如何询问它的副本是否仍然最新。本文通过本站点的实际响应来读这两只钟,并说明各自真正的用途。
新鲜度:多久不需要请求就能信任一个字节
新鲜度是决定一个请求是否直接从缓存命中的钟。它通常以 Cache-Control(max-age)或 Expires 日期表达。在新鲜度窗口内,缓存直接返回存储的响应,从不接触源。它的工作是降低延迟与源负载;代价是在窗口内可能提供略微落后于源的版本。
对静态站点,新鲜度窗口与不可变产物是吻合的:一次发布替换整棵树,因此短的新鲜度加一个校验器是安全、平淡的默认值。关键是新鲜度本身无法在过期后检测陈旧内容——那是第二只钟的工作。
校验:客户端如何询问"它仍然是最新的吗?"
当新鲜度过期,客户端并不一定重新下载整个资源。校验就是条件请求:发送存储的标识,源要么返回新的表示,要么返回一个状态表明存储的副本仍然有效。HTTP 定义的两个标识是 ETag(一个不透明校验器,通常由实体的字节派生)和 Last-Modified(一个时间戳校验器)。
本站的活动响应把两只钟都具体展示出来:
etag: "dkx84jwth7nkc9e"
last-modified: Mon, 24 Aug 2026 00:00:00 GMT
针对该 ETag 的条件 If-None-Match 请求产生 304 Not Modified——校验器一致,因此客户端复用缓存副本而不重新下载正文。这就是第二只钟完成其工作:一次廉价的往返,而不是一次完整传输。
为什么新鲜度与校验相互独立
两者必须分开理解,因为彼此并不蕴含。一个响应可以处于新鲜状态无需校验,然而校验标识仍然重要,为新鲜度过期那一刻做准备。反之,你也可以有强校验器,但一个导致几乎不变的资源做不必要重新验证的新鲜度策略。对静态文件,两者容易协调设定:短或公用的新鲜度、从产物字节派生的强 ETag,以及新鲜度一过客户端就走的重新验证路径。
要警惕的失败模式是把两者折叠成"缓存过期了,所以重新下载"。那忽略了校验器,把每过一次期都变成一次完整传输。压缩后的产物让往返变小——本页 HTML 只传输几千字节——但浏览器校验器的原则(缓存拒绝陈旧,同时避免冗余的完整正文传输)与大小无关。
值得断言的契约
一旦能分开命名新鲜度与校验,发布检查就能断言两者:被服务的响应头同时包含 etag 与 last-modified;新鲜度窗口足够短以保证诚实;当校验器匹配时,条件请求返回 304。这三项检查描述了一个既不会静默提供长久陈旧字节、也不会浪费带宽的缓存。这是一个比仪表盘曲线更弱的证明词,而这正是它应该进决策日志而非监控图的原因。
来源
- MDN:HTTP 缓存,过期与校验作为不同机制,已核验 2026-08-14。
- MDN:HTTP
ETag,条件请求校验,已核验 2026-08-14。 - RFC 9111:HTTP Caching,缓存过期与校验规则,已核验 2026-08-14。