打开 DevTools 的 Network 面板,会看到有些资源显示 200 (from disk cache), 有些显示 304 Not Modified。这两者都代表"没从服务器重新下载完整内容", 但背后的机制完全不同。理解它们,是配置好静态资源缓存策略的前提。

强缓存:本地直接用,不请求服务器

强缓存由响应头 Cache-Control 控制(旧规范里的 Expires 已基本被取代)。 命中时浏览器直接用本地副本,不发任何请求,因此 Network 面板里显示 from memory cachefrom disk cache

# 服务器响应头示例
Cache-Control: max-age=31536000   # 缓存 1 年(单位:秒)
Cache-Control: max-age=60, public # 缓存 1 分钟,允许所有中间缓存(CDN)使用
Cache-Control: no-cache           # 每次都需向服务器确认(协商),但不代表"不缓存"
Cache-Control: no-store           # 真正的不缓存:响应不落盘,每次全量下载

一个常见的误区:no-cache 字面意思是"不缓存",实际含义恰恰相反—— 它允许缓存,但每次使用前都要向服务器验证(走协商缓存流程)。 真正禁止缓存的是 no-store

协商缓存:问一下服务器,变了再下载

当强缓存失效(或响应头带了 no-cache)时,浏览器会带上上次的"凭证"去问服务器 "这个文件变了吗?"服务器返回 304 表示没变,浏览器继续用本地副本; 返回 200 + 新内容则表示变了。

凭证有两对:

  • ETag / If-None-Match(优先级更高): 服务器给内容生成一个指纹(如内容的 hash),浏览器下次请求时带上 If-None-Match: "abc123",服务器比对后决定返回 304 还是 200;
  • Last-Modified / If-Modified-Since: 基于文件最后修改时间,精度到秒,且可能因内容"改回去"而误判,因此现在一般优先用 ETag。

一次完整的缓存流程

第一次请求
  浏览器 ──GET /app.js──▶ 服务器
  浏览器 ◀─200 + Cache-Control: max-age=60 + ETag: "v1"── 服务器
  浏览器 保存到本地缓存

第二次请求(60 秒内)
  浏览器 ──直接使用本地副本──▶(无网络请求)→ 200 from disk cache

第三次请求(超过 60 秒)
  浏览器 ──GET /app.js,If-None-Match: "v1"──▶ 服务器
  浏览器 ◀─304 Not Modified(无 body)── 服务器
  浏览器 继续使用本地副本

实践建议

  • 带 hash 的静态资源(如 app.8f3a2b.js):内容变了文件名就变, 可以放心设置 max-age=31536000, immutable,一年都不需要重新验证;
  • HTML 文档:建议 no-cache,保证每次都能拿到最新的页面引用(进而拿到最新的资源 hash);
  • API 响应:按业务决定,实时性要求高的数据用 no-store 或短 max-age;
  • CDN 配合:给静态资源加 public 指令,让 CDN 节点也能缓存,源站压力更小。

总结:强缓存省请求,协商缓存省流量。把"带 hash 的资源长缓存、HTML 走协商"当成默认策略,就抓住了缓存的 80%。