文章

浏览器缓存策略深度解析

强缓存 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:谁更可靠?

 ETagLast-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。

本文由作者按照 CC BY 4.0 进行授权