我不知道的HTTP(03)— 浏览器缓存:强缓存、协商缓存与整条链路
很多人以为浏览器缓存就是"强缓存和协商缓存两种",面试能说出这两个词就算过关。但实际上,缓存不是一个二选一的开关——它是一条从客户端到代理再到源服务器的完整链路,每一级都有自己的缓存策略,每一级都可能返回不同的响应。
一、问题引入
很多人以为浏览器缓存就是”强缓存和协商缓存两种”,面试能说出这两个词就算过关。但实际上,缓存不是一个二选一的开关——它是一条从客户端到代理再到源服务器的完整链路,每一级都有自己的缓存策略,每一级都可能返回不同的响应。
把缓存理解成”两种类型”,就像把交通理解成”红灯和绿灯”——知道了规则,但看不见整条路。
二、表面认知:大多数人停在这里
让一个前端开发者解释浏览器缓存,回答大概率是:
“强缓存直接用本地的,不发请求;协商缓存要问一下服务器,服务器说没变就用缓存。”
这个说法不算错,但它有两个盲区。
盲区一:它把缓存简化成了客户端和服务器之间的事,忽略了中间可能存在的代理层(CDN、Nginx 反向代理)。现实世界中,绝大多数请求根本到不了源服务器——CDN 边缘节点就把活干完了。
盲区二:它把 Cache-Control 的几个指令混为一谈。no-cache 和 no-store 差一个单词,含义完全不同;must-revalidate 看名字像”必须重新验证”,但它只在缓存过期后才生效。这三个指令的区别,是面试中被问最多的缓存知识点。
三、往下挖:缓存链路与控制机制
3.1 Cache-Control:缓存的控制中枢
Cache-Control 是 HTTP 缓存体系的核心响应头,它告诉浏览器和中间代理:这个响应能不能缓存、缓存多久、过期了怎么办。
说白了,Cache-Control 就是一组指令的集合,每条指令回答一个具体问题。最关键的三条指令经常被混淆,需要逐个拆开看。
no-store:完全禁止缓存。浏览器和代理都不允许存储这个响应的任何副本,每次请求都必须从源服务器获取完整响应。适用于支付页面、密码修改等场景。
no-cache:允许缓存,但每次使用前必须向服务器验证。浏览器会存储响应副本,但在使用之前一定会发一个条件请求确认资源是否有更新。名字里带个”no”,但它不是”不缓存”,而是”不跳过验证”。
must-revalidate:缓存在有效期内可以直接使用,过期后必须向服务器验证,不能使用过期的缓存副本。和 no-cache 的区别在于:no-cache 每次都验证,must-revalidate 只在过期后才验证。
| 指令 | 是否存储 | 何时验证 | 典型场景 |
|---|---|---|---|
no-store | 否 | 不适用 | 支付页面、敏感数据 |
no-cache | 是 | 每次使用前 | HTML 入口文件、API 响应 |
must-revalidate | 是 | 过期后 | 需要一致性保证的静态资源 |
这里有一个很多人会忽略的细节——max-age 的计时起点是响应生成的时间,不是浏览器收到响应的时间。如果响应在代理服务器上缓存了 30 秒才转发到浏览器,那浏览器拿到的 max-age=60 实际上只剩 30 秒有效期。HTTP 通过 Age 响应头传递这个信息:Age: 30 表示响应已在中间缓存中停留了 30 秒。
3.2 强缓存:根本不发请求
当浏览器判断本地缓存仍在有效期内(max-age 未过期),就直接使用缓存副本,不会向服务器发送任何请求。这就是强缓存。
在 Chrome DevTools 的 Network 面板中,强缓存命中的请求状态码是 200,但 Size 列显示 (from disk cache) 或 (from memory cache)——内存缓存速度更快但随标签页关闭而消失,磁盘缓存持久但读取稍慢。一般来说,小文件倾向于内存缓存,大文件倾向于磁盘缓存,具体策略由浏览器自行决定。
问题的关键在于——强缓存虽然性能最好(零网络开销),但一旦缓存了就无法主动让用户获取最新版本。这就是为什么静态资源通常在文件名中加入内容哈希(如 style.a3b2c1.css):内容变了文件名就变了,浏览器会当作新资源请求,老缓存自然失效。
3.3 协商缓存:问一下再决定
当缓存过期,或者响应头设置了 no-cache,浏览器不会直接丢弃本地副本,而是带着验证信息向服务器”协商”:这个资源变了没有?
验证机制有两种,可以单独使用也可以组合使用。
基于时间:服务器在响应中返回 Last-Modified 头(资源最后修改时间),浏览器下次请求时通过 If-Modified-Since 头带上这个时间。服务器对比后,如果资源没有变化,返回 304 Not Modified(不带响应体),浏览器继续使用本地缓存。
基于内容:服务器在响应中返回 ETag 头(资源的唯一标识,通常是内容的哈希值),浏览器下次请求时通过 If-None-Match 头带上这个值。服务器对比后,ETag 一致则返回 304。
下面这段代码演示了一个最简的协商缓存服务端实现,可以直接用 Node.js 运行:
const http = require('http');
const crypto = require('crypto');
const content = '<h1>Hello</h1>';
const etag = crypto.createHash('md5').update(content).digest('hex');
http
.createServer((req, res) => {
if (req.headers['if-none-match'] === etag) {
res.writeHead(304);
res.end();
return;
}
res.writeHead(200, {
'Content-Type': 'text/html',
ETag: etag,
'Cache-Control': 'no-cache',
});
res.end(content);
})
.listen(3000);
第一次访问返回 200 和完整内容;第二次访问时浏览器自动带上 If-None-Match 头,服务器对比 ETag 后返回 304——在 DevTools 的 Network 面板中,304 响应的 Size 列只有几十字节(只有响应头没有响应体),Time 列也明显短于完整响应。这就是协商缓存节省带宽的直观体现。
换句话说,ETag 和 Last-Modified 的根本区别在于:Last-Modified 精度只到秒,一秒内的多次修改无法区分;ETag 基于内容生成,只要内容变了就一定能检测到。当两者同时存在时,ETag 优先级更高。
3.4 整条缓存链路:不只是浏览器的事
缓存不只发生在浏览器端。一个完整的 HTTP 缓存链路是:客户端(浏览器) → 代理(CDN / 反向代理) → 源服务器,每一级都可以缓存,每一级都有自己的策略。
Cache-Control 的一些指令专门用来控制代理行为。
public 和 private:public 允许代理缓存这个响应(CDN 就能缓存它),private 限定只有浏览器可以缓存。包含用户个人数据的响应应该用 private,避免被共享代理缓存后泄露给其他用户。
s-maxage:专门给代理用的过期时间,覆盖 max-age。比如 Cache-Control: max-age=60, s-maxage=3600 意味着浏览器缓存 1 分钟,CDN 缓存 1 小时。这在实际部署中非常常用——CDN 长缓存减少回源次数,浏览器短缓存保证用户能较快看到更新。
一个典型请求经过缓存链路的完整路径是这样的:浏览器检查本地缓存,命中且未过期就直接使用(强缓存);未命中或过期,请求到达 CDN 边缘节点;CDN 检查自己的缓存,命中则返回;CDN 也未命中,才回源到服务器获取最新响应,并沿链路逐级更新缓存。
如果你只记住一句话,记住这个:绝大多数用户请求不需要到达源服务器——浏览器缓存和 CDN 缓存联合起来,已经覆盖了大部分场景。
3.5 两个值得关注的新指令
immutable:告诉浏览器这个资源在有效期内绝对不会变化,用户按 F5 刷新页面时也不需要发起条件请求验证。没有 immutable 时,即使 max-age 未过期,刷新操作仍然可能触发条件请求。适合文件名中已包含内容哈希的静态资源。
stale-while-revalidate:允许浏览器在缓存刚过期的一段窗口期内,先返回过期的缓存让用户不必等待,同时在后台异步去服务器获取最新版本。比如 Cache-Control: max-age=3600, stale-while-revalidate=60 表示缓存 1 小时有效,过期后 60 秒内允许先用旧的、后台更新。对实时性要求不高的场景,这个策略能显著减少用户等待时间。
四、这意味着什么:实际项目中的缓存策略
理解了缓存链路和控制机制,可以总结出一套实际项目中常用的策略搭配。
(1) HTML 入口文件:设置 Cache-Control: no-cache。每次都验证,保证用户总能拿到最新的 HTML,而 HTML 中引用的资源 URL 包含哈希值,自然会指向正确的版本。
(2) 带哈希的静态资源(JS、CSS、图片):设置 Cache-Control: max-age=31536000, immutable。一年长缓存,内容变了文件名就变了,不需要验证。这是目前最被广泛采用的静态资源缓存方案。
(3) API 响应:根据业务需求选择——no-store(敏感数据)、no-cache(需要实时性)、或短时间的 max-age(允许一定延迟)。
(4) CDN 层面:通过 s-maxage 控制 CDN 缓存时长,通常比浏览器的 max-age 更长。HTML 在 CDN 上可以设较短的 s-maxage(比如 60 秒),既利用了 CDN 分发优势,又不会让用户看到太旧的内容。
说白了,缓存策略的核心思路就是一句话:能长缓存的用哈希文件名 + 长 max-age,不能长缓存的用 no-cache + ETag 验证,敏感数据用 no-store 彻底不存。
五、总结
浏览器缓存不是”强缓存和协商缓存”的二选一,而是一条从浏览器到 CDN 到源服务器的完整链路——Cache-Control 的每一条指令,都在决定这条链路上哪一级响应、何时响应、如何响应。
本系列其他文章:
- 上一篇:Cookie到底是怎么工作的
- 下一篇:HTTPS握手:TLS到底做了什么
相关主题:
- CDN 的缓存与 DNS 智能调度如何配合?见本系列第 01 篇:从输入URL到页面呈现
- HTTP/2 的多路复用对缓存策略有什么影响?见本系列第 05 篇:从HTTP/1.1到HTTP/3:队头阻塞是如何被消灭的