HTTP 缓存:把 Cache-Control 讲明白
这篇是我某次排查「文件名没变但内容变了,用户一直看到旧图」时整理的。 写下来之后发现,很多混乱其实来自一个误解:以为缓存是「浏览器自己决定要不要存」,实际上几乎全由响应头说了算。
两种缓存,先后顺序是固定的
浏览器收到请求时不是「判断要不要缓存」,而是先看有没有没到期的本地副本:
- 强缓存:本地副本还在有效期内,直接用它,不发任何网络请求。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 不等于不缓存,
剩下的都是细节。