浏览器缓存策略深度解析
强缓存 vs 协商缓存,Cache-Control 怎么配,ETag 和 Last-Modified 该用哪个。
一句话概括
浏览器缓存分两层:强缓存(没过期直接本地读,0 网络开销)和协商缓存(过期后带 ETag/Last-Modified 问服务器”变了没”,没变返回 304 继续用)。合理配置能减少 90%+ 的请求量,是前端性能优化的第一课。
核心知识点
1. 强缓存:一次请求都不发
由 Cache-Control: max-age=秒 控制。在 max-age 时间内,浏览器直接从 memory/disk cache 读取,状态码 200 (from disk cache),网络请求数为 0。
1
2
3
4
5
# 响应头示例
Cache-Control: public, max-age=31536000, immutable
# public: 可被 CDN 缓存
# max-age=31536000: 一年
# immutable: 告诉浏览器"这个资源绝对不会变",禁止用户刷新时重新验证
max-age 优先级高于 Expires。Cache-Control: no-store 直接禁用缓存,no-cache 则允许缓存但每次必须验证。
2. 协商缓存:发个请求但省下响应体
缓存过期后,浏览器带上缓存的标识去问服务器,服务器决定返回 304(用缓存)还是 200(发新资源)。
1
2
3
4
5
6
7
8
// 浏览器发的请求头
If-None-Match: "abc123" // 对应 ETag
If-Modified-Since: Mon, 01 Jun 2026 00:00:00 GMT // 对应 Last-Modified
// 服务器判断资源没变 → 只返回 header,body 为空
HTTP/1.1 304 Not Modified
ETag: "abc123"
// ← 响应体为空,省掉了所有传输成本
304 响应本身只消耗几十字节的 header,比重新下载整个文件(可能几百 KB)划算得多。
3. ETag vs Last-Modified:谁更可靠?
| ETag | Last-Modified | |
|---|---|---|
| 粒度 | 内容哈希,字节级精确 | 秒级时间戳 |
| 1 秒内多次修改 | ✅ 能检测 | ❌ 检测不到 |
| 内容没变但 mtime 变了 | ✅ 哈希不变 | ❌ 误判为修改 |
| 分布式服务器 | 需保证同内容同哈希 | ⚠️ 各机器时间可能不同步 |
| 计算成本 | 需读文件算哈希 | 几乎无成本 |
结论:ETag 优先级高于 Last-Modified,两者同时存在时浏览器优先用 ETag。nginx 默认两者都发,If-None-Match 和 If-Modified-Since 同时带。
1
2
3
4
5
6
# nginx 典型配置
location ~* \.(js|css|png|jpg|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
etag on; # 默认开启,nginx 自动生成文件内容哈希
}
4. 缓存决策树
1
2
3
4
5
6
7
需要缓存这个资源吗?
├─ 完全不能缓存 → Cache-Control: no-store (如用户隐私数据)
├─ 能缓存但每次验证 → Cache-Control: no-cache (如 HTML 入口文件)
├─ 能缓存一段时间 → Cache-Control: max-age=N + ETag
│ ├─ 带 hash 的静态资源 → max-age=一年 + immutable
│ └─ 可能变但可接受延迟 → max-age=几分钟到几小时
└─ CDN 也可以缓 → 加 public
5. 文件指纹策略:immutable 的最佳搭档
Webpack/Vite 构建的 app.a3f8c2.js 文件名带 content hash——内容不变 hash 不变,内容变了 hash 跟着变。这种资源永远不会原地修改,配 max-age=31536000, immutable 最合适。
1
2
3
4
5
6
7
// webpack.config.js
output: {
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].chunk.js'
}
// → 构建产出 app.a3f8c2d1.js
// → 配一年强缓存 + immutable,完美
而 HTML 入口文件(index.html)不能这么干——它的引用会变,必须每次验证:Cache-Control: no-cache。
其实你每天都在用
- 刷新页面的三种姿势:普通刷新(F5)走协商缓存,强制刷新(Ctrl+Shift+R)跳过缓存全量下载,地址栏回车优先走强缓存
- CDN 加速:jsdelivr、unpkg 上的第三方库都用了一年期强缓存,第二次访问秒开
- Vite 开发服务器:
node_modules下的依赖做了预构建 + 强缓存,只有源码变更才重新编译 - Service Worker + Cache API:PWA 离线可用本质是把资源缓存到 SW 里,离线也能从缓存读
- 浏览器 back/forward 缓存(bfcache):前进后退页面瞬间恢复,连 JS 执行状态都保留
常见误解(FAQ)
❌ 误区一:”no-cache 就是不缓存”
no-cache 允许缓存,但每次使用前必须向服务器验证(走协商缓存)。真正不缓存的是 no-store。面试问”no-cache 和 no-store 的区别”就是在考这个。
❌ 误区二:”协商缓存比强缓存差,304 也占一次请求”
304 只传输 header(几十字节),比重新下载整个文件快得多。而且对 HTML 入口文件来说,必须用协商缓存才能及时感知更新——index.html 配强缓存会导致用户拿到过期版本,白屏很久。
❌ 误区三:”ETag 是内容哈希,所以同一份文件在任意服务器上 ETag 都一样”
错。nginx / Apache 的默认 ETag 是基于文件的 inode + size + mtime 生成的(nginx 是十六进制拼接成 inode-size-mtime)。inode 是文件系统层面的编号,同一份文件拷贝到不同机器,inode 必然不同。所以多机部署或走多 CDN 节点时,同一文件的 ETag 可能不一致,导致协商缓存失效。跨服务器部署要关掉默认 ETag(nginx 用 etag off),改用基于内容哈希的 ETag,或只依赖 Last-Modified 做协商。
❌ 误区四:”加了 max-age 就万事大吉”
max-age 只控制浏览器缓存时间,不控制 CDN 缓存时间。CDN 看 s-maxage,如果不设置 s-maxage 才回退到 max-age。另外 immutable 需要浏览器支持(Chrome 49+,Safari 11+),不支持的浏览器会忽略它。
一句话总结
浏览器缓存不是”能不用就不用”的优化手段,而是”必须有一份合理策略”的基础设施。记住四个决策:HTML 用 no-cache,带 hash 的静态资源用一年 + immutable,API 响应按业务需要设 max-age,用户私有数据用 no-store。