← 返回文章

浏览器 / 前端性能 / HTTP

前端 HTTP 缓存实战:Cache-Control、ETag 与版本化资源

前端缓存问题常常表现为两个极端:静态资源每次刷新都重新下载,页面慢且浪费流量;新版本已经上线,用户却仍看到旧 HTML,甚至出现“旧页面引用新脚本”或“新页面引用已删除资源”的白屏。

HTTP 缓存不是简单的“开或关”。正确策略要区分 HTML、带内容哈希的静态资源、公开接口和私密响应,再组合 Cache-Control、验证器与稳定的发布流程。

一、先区分新鲜缓存与协商缓存

浏览器收到可缓存响应后,会保存内容和缓存元数据。下一次请求时大致有两条路径:

  1. 缓存仍然新鲜,直接复用,不访问服务器。
  2. 缓存需要验证,携带 ETag 或 Last-Modified 请求服务器;内容没变时返回 304。

两者都叫“用了缓存”,但网络成本不同。新鲜缓存最快;304 不传完整响应体,仍然需要一次网络往返。

二、Cache-Control 最重要的几个指令

max-age

1
Cache-Control: public, max-age=3600

响应在 3600 秒内可视为新鲜。浏览器通常可以直接复用。

no-cache

1
Cache-Control: no-cache

no-cache 不是“不允许存储”。它表示复用前必须向源站验证。配合 ETag,未变化时可以得到 304。

no-store

1
Cache-Control: no-store

不应存储响应。登录凭证、支付结果、包含高度敏感个人数据且不应留在缓存中的响应可以考虑使用。不要为了“保证最新”给所有资源都用 no-store,那会完全失去缓存收益。

private 与 public

1
Cache-Control: private, max-age=60

private 允许浏览器等私有缓存保存,但不允许共享缓存把响应提供给其他用户。包含用户定制内容的页面通常需要 private。

public 则明确允许共享缓存,在 CDN 场景更常见,但仍要结合鉴权、Cookie 和响应内容判断,不能把用户数据误设成 public。

三、带内容哈希的静态资源可以长期缓存

现代构建工具通常生成:

1
2
app.4f9c2a1.js
styles.a8810d3.css

文件内容变化,文件名也变化。旧文件可以放心长期缓存:

1
Cache-Control: public, max-age=31536000, immutable

immutable 告诉客户端在新鲜期内不必为了刷新动作重新验证。它适合内容寻址资源,不适合固定文件名的 app.js。如果固定 URL 的内容会更新,却缓存一年,用户就可能一年拿不到新版本。

长期缓存需要发布系统保留一段时间的旧资源。新 HTML 部署后,已打开的旧页面可能仍会动态加载旧 chunk;立即删除所有旧文件会造成运行时 404。

四、HTML 通常需要每次验证

入口 HTML 决定当前应该加载哪些版本化资源,因此不宜像哈希脚本一样缓存一年。常见策略是:

1
2
Cache-Control: no-cache
ETag: "index-v42"

浏览器可以保存 HTML,但再次访问时先验证。内容未变返回 304,内容变化则拿到新 HTML。

对于必须始终从网络获取且不应存储的页面才使用 no-store。大多数公开站点 HTML 使用 no-cache 加验证器更平衡。

部署顺序也很重要:

  1. 先上传新的哈希静态资源。
  2. 确认资源可访问。
  3. 最后发布引用它们的新 HTML。
  4. 保留旧资源,直到旧页面自然退出。

这样可以避免 HTML 指向尚未上传的脚本。

五、ETag 与条件请求

服务器可以为响应提供实体标签:

1
ETag: "a1b2c3"

客户端再次验证时发送:

1
If-None-Match: "a1b2c3"

内容未变化,服务器返回:

1
HTTP/1.1 304 Not Modified

304 不包含完整响应体,浏览器继续使用本地副本。ETag 应由内容或稳定版本生成,不要每次请求都产生不同值,否则验证永远无法命中。

Last-Modified/If-Modified-Since 也能验证,但时间精度和内容语义通常不如 ETag 灵活。两者如何生成应由服务器或 CDN 统一管理。

六、API 响应要按数据性质设计

公开、不频繁变化的商品分类可以短缓存:

1
Cache-Control: public, max-age=60, stale-while-revalidate=300

用户资料则可能是:

1
Cache-Control: private, no-cache

一次性敏感响应可能是:

1
Cache-Control: no-store

缓存键还要考虑 URL 查询参数、请求头和用户身份。不能只看到 GET 就认为可以 public 缓存。

Vary 告诉缓存哪些请求头会改变响应:

1
Vary: Accept-Encoding, Accept-Language

如果服务端按语言返回不同内容,却没有正确 Vary,共享缓存可能把中文响应发给英文用户。Vary 维度过多又会降低命中率,因此只加入真正影响表示的请求头。

七、浏览器 HTTP 缓存和 Service Worker 不是一回事

Service Worker 的 Cache Storage 由应用脚本显式控制;HTTP 缓存由响应头和浏览器策略控制。两者可以同时存在。

如果 Service Worker 使用 cache-first 返回旧文件,修改服务器 Cache-Control 也不一定立即生效,因为请求可能根本没到 HTTP 层。排查时要分别检查:

  • 页面是否被 Service Worker 控制;
  • Cache Storage 中存了什么;
  • 网络请求是否真正发出;
  • HTTP 响应头和 Age 是什么;
  • CDN 是否还有一层缓存。

不要把所有“刷新后没更新”都归因于浏览器缓存。

八、开发工具中的常见误判

DevTools 的 Disable cache 通常只在工具打开时生效。它适合排除缓存影响,不代表真实用户环境。

强制刷新也不是缓存策略测试。验收时应关闭 Disable cache,按正常访问流程检查:

  1. 首次加载哪些资源返回 200?
  2. 第二次加载哪些来自 memory/disk cache?
  3. HTML 是否发出条件请求并正确获得 304 或新内容?
  4. 新版本发布后,旧 HTML 是否能安全运行?
  5. 用户私密响应是否误入共享缓存?

九、一套可直接使用的策略

1
2
3
4
5
6
HTML:                 no-cache + ETag
哈希 JS/CSS/字体: public, max-age=31536000, immutable
普通图片: 根据更新频率设置 max-age,最好版本化
公开查询 API: 短 max-age,可评估 stale-while-revalidate
用户个性化 API: private, no-cache
高度敏感响应: no-store

这只是起点,最终仍要根据更新频率、数据风险、CDN 行为和回滚策略调整。

十、上线检查清单

  • 静态文件名是否包含内容哈希?
  • HTML 是否避免了过长新鲜期?
  • no-cache 和 no-store 是否使用在正确语义上?
  • ETag 是否稳定,304 是否真实命中?
  • 用户数据是否被错误标记为 public?
  • Vary 是否覆盖真正影响内容的请求头?
  • Service Worker 和 CDN 是否有独立缓存规则?
  • 发布时是否先上传资源、后更新 HTML并保留旧 chunk?
  • 回滚后旧 HTML 与静态资源是否仍能组合工作?

缓存优化不是把 max-age 调大,而是让每类资源拥有可证明的更新策略。入口文档保持可验证,内容哈希资源长期缓存,私密数据限制共享,再把发布顺序和旧资源保留纳入部署流程,前端才能同时获得速度与更新可靠性。

参考资料