缓存的两个层次
浏览器缓存主要分两类,工作方式完全不同:
强缓存
服务器在响应头里告诉浏览器"这个资源在某个时间之前不用再来问"。浏览器把这个规则记下来,在有效期内直接使用本地副本,完全不发请求。这是最快的情况——耗时接近于零。
控制强缓存的响应头有两个:Cache-Control: max-age=3600 表示 3600 秒内有效;Expires 是旧式写法,指定一个绝对时间点。现代站点优先使用 Cache-Control。
协商缓存
强缓存过期后,浏览器会向服务器发一个"问询"请求:我本地这份是某个版本,还能用吗?服务器回答"可以"(返回 304,无内容)或"不行,用新的"(返回 200 和新内容)。
协商缓存的判断依据有两个:ETag(内容指纹)和 Last-Modified(最后修改时间)。用 ETag 更准确,因为时间戳可能因为部署方式变化而失真。
协商缓存的往返仍然需要一次网络请求,但节省了传输内容的开销。所以它的速度介于"强缓存命中"和"完全重新下载"之间。
资源指纹的作用
强缓存有一个固有矛盾:缓存时间设得太短,每次都要重新验证,起不到加速作用;设得太长,内容更新后用户还在用旧版本,出问题。
解决办法是给文件名加指纹。比如把 app.js 改成 app.a1b2c3.js,其中 a1b2c3 是文件内容的哈希值。这样:
- 内容不变 → 文件名不变 → 可以设置极长的缓存时间(比如一年)
- 内容改变 → 文件名改变 → 浏览器视为全新资源,自动重新下载
这就是为什么很多站点的资源文件名里带一串看不懂的字符。它不是随机数,而是内容的指纹。这套机制让"永久缓存"和"即时更新"可以同时成立。
但有个前提:HTML 文件本身不能设长缓存。因为 HTML 里引用了哪些资源文件名是变化的,如果 HTML 被缓存了,浏览器就永远不知道资源换了名字。所以标准做法是 HTML 用短缓存或不缓存,资源文件用超长缓存。
缓存导致的常见问题
缓存虽然加速,但也制造了一类典型故障:更新后部分用户看到旧内容。原因通常是某一层缓存没刷新。可能的位置包括:浏览器本地缓存、CDN 边缘节点缓存、反向代理缓存。
排查方法是看响应头里的标记:X-Cache、CF-Cache-Status、Age 等字段会说明这次响应来自缓存还是源站。如果命中缓存但内容应该是新的,就需要在对应层级做刷新。
另一种常见情况是缓存了不该缓存的内容。比如按用户区分的页面被 CDN 当成公共资源缓存,导致 A 用户看到 B 用户的内容。这类问题通常通过设置 Cache-Control: private 或者按 Cookie 分流来解决。
常见疑问
问:清除缓存会不会丢数据?
答:清除"缓存"通常只影响临时文件,不丢登录状态。但清除"网站数据"或"Cookie"会丢失登录状态,因为那些不是缓存而是本地存储。两者在设置界面里常常被放在一起,需要注意区分。
问:为什么手机上清了缓存还是旧页面?
答:可能中间还有一层 CDN 缓存,或者页面被 Service Worker 接管了。后者需要在开发者工具里单独注销 Service Worker 才能清除。
问:无痕模式有缓存吗?
答:有,但关闭窗口时清除。所以无痕模式下第一次访问同样慢,刷新之后会变快,只是这个缓存活不过本次会话。