HTTP 缓存策略速览
从 Cache-Control 到条件请求,一张图理清浏览器缓存的关键开关。
浏览器缓存是 Web 性能里「投入产出比」最高的一环:不加缓存,一次页面刷新就要把脚本、样式、图片全部重传;配好缓存,大部分资源直接从本地读取。本文不用面试题的口气罗列知识点,只讲清楚几个开关各管什么、怎么配合。
一切从 Cache-Control 开始
响应头 Cache-Control 是缓存的总开关,几乎所有策略都从这里起步。最常见的取值是 max-age,它告诉浏览器:这份资源在接下来多少秒内可以直接复用,不用再问服务器。
举个例子,给静态图片设置一天的缓存:
HTTP/1.1 200 OK
Content-Type: image/jpeg
Cache-Control: public, max-age=86400
max-age=86400 表示 24 小时内图片都不会过期。这里要注意两点:
- 取值单位是秒,不是毫秒,
86400恰好是一天; public允许任何中间代理(CDN、网关)缓存,private则只允许浏览器缓存。
几个容易混淆的指令
下面这几个值经常一起出现,但含义不同,值得单独拎出来:
禁止缓存与禁止复用
no-store 最彻底——告诉所有节点不要存任何副本,常用于登录态、支付信息这类敏感数据。而 no-cache 名字有迷惑性,它其实允许缓存,只是每次用之前都先回服务器验证一下(见下一节的「条件请求」)。
用一句话记:
no-store是「根本别存」,no-cache是「存了但每次都要验」。
缓存过期了怎么办:条件请求
max-age 只解决「多久过期」,过期之后浏览器不会直接丢弃,而是发起一次条件请求,让服务器判断资源是否真的变了。这个过程有两条载体,一个是 ETag,一个是 Last-Modified。先从整体看一眼这张流程图:

图中这条路可以拆成三段,下面依次展开:先看指纹,再看对账,最后讲兜底。
三段式拆解
- 到期前:本地直接命中,零请求
- 判断依据:
max-age还没走完 - 备注:
must-revalidate可禁用过期后的「陈旧复用」
- 判断依据:
- 到期后:发条件请求去对账
- 载体一:
If-None-Match+ETag - 载体二:
If-Modified-Since+Last-Modified
- 载体一:
- 对账失败:服务器返回完整 200 正文
ETag:内容的指纹
服务器返回资源时,可以附带一个 ETag 头,本质是内容的哈希,比如 "abc123"。浏览器把它存下来,过期后重新请求时带上 If-None-Match: "abc123":
GET /app.js HTTP/1.1
Host: example.com
If-None-Match: "abc123"
服务器比对后,如果没变,就返回一个没有正文的 304 响应,浏览器得知货没换,直接用本地副本就行。这个交互可以用一张简单的时序表概括:
| 请求头 / 响应头 | 归属方 | 作用 |
|---|---|---|
Cache-Control: max-age=86400 | 响应 | 规定缓存存活多久 |
ETag: "abc123" | 响应 | 对内容取指纹 |
If-None-Match: "abc123" | 请求 | 拿旧指纹去对账 |
304 Not Modified | 响应 | 没变,省下传输正文 |
Last-Modified:退而求其次
Last-Modified 记录文件最后修改时间,配合请求头 If-Modified-Since 使用,原理类似。它的精度只有秒,同一秒内改两次就会漏判,所以口碑不如 ETag。实践中两者也可以同时给出,服务器一般优先校验 ETag,再退而看时间戳。两者的取舍对比,可以看这篇 RFC 7232 摘要。
实战里怎么选
不同资源适合不同的策略,别一刀切:
- 带版本号的静态资源,如
app.a1b2c3.js,放心设很长的max-age——文件名变了,URL 就变,不会吃到旧缓存; - HTML 页面本身要设短缓存或
no-cache,否则改完上线浏览器还在用旧壳; - 用户数据接口,谨慎用
no-store,避免隐私泄到公共缓存。
// 服务端设置响应头(示意)
res.set('Cache-Control', 'public, max-age=31536000'); // 一年 = 365 * 24 * 60 * 60
res.set('ETag', `"${hashOf(res.body)}"`);
再配合一张检查清单,上线前逐条过一遍会更稳妥:
- 静态资源带版本号,并设了长
max-age - HTML 页面设置了
no-cache - 接口确认是否加 ETag 做条件请求
- 敏感数据确认是
no-store
其他容易漏的细节
缓存相关要补的知识点还不少,随手列几条备忘:
Vary头:比如Vary: Accept-Encoding,告诉缓存器「按压缩方式区分缓存」,否则 gzip 和原始文件会串台;- 浏览器地址栏刷新和「强制刷新」行为不同——后者接近绕过缓存;
- 私有缓存(浏览器)和共享缓存(代理)对
private/public的响应不同; - 有些框架会自动带
ETag,但只对/这类少数路由生效,别默认全覆盖。
其中第 3 条值得展开,用嵌套引用示意思考层级:
缓存不只是浏览器本地的事。
你还需要考虑 CDN 和反向代理,它们都是共享缓存层。
给它们看的指令,可能和给浏览器看的不一样。
调试时最常用的快捷键是 F12 打开开发者工具,切到 Network 面板。把某个响应标成灰色,多半就是从本地缓存读的了。
结尾
缓存调优没有银弹,本质是在「新鲜度」和「传输成本」之间找平衡。把 Cache-Control 当总纲、把条件请求当兜底,多数站点的问题就能解决大半。更完整的定义可以去翻 MDN,见文末脚注。
顺带说明,有的资料里会把 max-age 写作 \*max-age\* 以强调它是必填字段,其实这只是排版写法,并不是真的星号——HTTP 头里没有「必填」这个概念,全靠约定。
如无特别说明,示例均以 HTTP/1.1 为背景;HTTP/2 之后的字段含义不变。
再提醒一句被问烂的话:缓存不是「无脑设 max-age 就完事」,它必须和你的更新流程配套才能不踩坑。上面清单里勾掉的两条,就是最关键的起步动作。
转载或引用请注明出处,本文基于 MDN 按需整理,非逐字翻译。1
Footnotes
-
ETag 与条件请求的完整机制,见:https://developer.mozilla.org/docs/Web/HTTP/Conditional_requests。 ↩