← 全部笔记

HTTP 缓存:把 Cache-Control 讲明白

2026-10-16 网络与协议 约 6 分钟

这篇是我某次排查「文件名没变但内容变了,用户一直看到旧图」时整理的。 写下来之后发现,很多混乱其实来自一个误解:以为缓存是「浏览器自己决定要不要存」,实际上几乎全由响应头说了算。

两种缓存,先后顺序是固定的

浏览器收到请求时不是「判断要不要缓存」,而是先看有没有没到期的本地副本:

  • 强缓存:本地副本还在有效期内,直接用它,不发任何网络请求。DevTools 里显示 (from disk cache) 或 (memory cache)。
  • 协商缓存:副本过期了,带上校验字段问服务器「这个还能用吗」。服务器说可以,返回 304,不带响应体。

所以强缓存命中时你连 304 都看不到,一次请求都没有。这点很关键——很多人以为「304 就是缓存生效」,其实那只是第二层。

谁控制哪一层

字段分工大致是这样,记住这张表比背文档快:

层 请求头 响应头
强缓存 Cache-Control Cache-Control、Expires
协商缓存 If-None-Match、If-Modified-Since ETag、Last-Modified

两个优先级要记:Cache-Control 存在时 Expires 被忽略; ETag 存在时 If-None-Match 优先于日期比较。原因很简单——日期只有秒级精度,一秒内改两次判断不出来。

最容易搞错的两个值

no-cache 和 no-store 名字像,含义差很远:

  • no-cache:可以存,但每次用之前必须问服务器。它其实是「强制走协商缓存」。
  • no-store:根本不存。给含隐私内容的响应才用。

写接口调试时常犯的错:给所有 API 加 no-store 图省事,结果连 304 都用不上,每次全量下载。真正想要的是 no-cache。

几个实战里踩到的坑

1. s-maxage 只对共享缓存生效

它管的是 CDN 和代理,浏览器不认。想要「CDN 缓 1 天、浏览器缓 5 分钟」就写:

Cache-Control: public, max-age=300, s-maxage=86400

2. 带 query 的 URL 在 CDN 上可能被当成不同资源

如果 CDN 的缓存键包含完整 query(默认常常是),那么 ?v=1 和 ?v=2 会各存一份,命中率掉到很低。 静态资源建议要么把版本做进文件名,要么在 CDN 上配置「忽略指定参数」。 反过来,如果参数确实影响内容,忽略它就是事故。

3. Vary 没配好会导致串内容

同一 URL 返回不同格式(比如 gzip 与非 gzip),CDN 若不分键,可能把压缩过的字节发给不支持的客户端。 服务器一般会自动加 Vary: Accept-Encoding,自己接管缓存时别忘了这一条。

4. 小文件才值得 immutable

Cache-Control: public, max-age=31536000, immutable 意味着一年内不再校验。 只有文件名带内容哈希时才敢这么用——文件名不变就代表内容不变,这个前提一破,用户就永远看不到更新。


一句话总结:先把「强缓存 / 协商缓存」两层顺序记牢,再记住 no-cache 不等于不缓存, 剩下的都是细节。