静态资源不能使用一条统一缓存规则。带内容指纹的 CSS、JavaScript 和字体可以长期缓存;HTML 要能较快发现新版本;未改名的图片只能在可接受的旧内容窗口内缓存;包含账号或支付信息的响应不应进入共享缓存。

HTML、带指纹资源、普通图片和敏感响应分别对应重新验证、长期缓存、短缓存和不存储的决策图
缓存时长由 URL 是否随内容变化、响应是否公开,以及旧内容可以保留多久共同决定。

缓存策略先看 URL 是否随内容变化

资源URL 变化规则常用策略发布要求
HTML地址长期不变,内容会更新no-cache 或较短 max-age能及时重新验证或过期
带内容指纹的 CSS/JS/字体内容变化时文件名变化public, max-age=31536000, immutableHTML 必须引用新 URL
未带版本的图片、PDF同一地址可能替换短缓存加 ETag更新后刷新 CDN 或更换 URL
公开 API 响应由业务语义决定明确 public/privates-maxageVary缓存键覆盖所有响应差异
敏感或一次性响应不允许被复用no-storeCDN 同样不得缓存

app.4f81c9a2.js 的内容变化后会生成新文件名,旧 URL 才能被视为不可变。只有目录叫 /assets/,并不能证明里面每个文件都不会原地更新。

Cache-Control 决定谁能存、何时复用

RFC 9111(在新标签页打开)定义了 HTTP 缓存语义;immutableRFC 8246(在新标签页打开)补充的响应指令。常见指令承担不同职责:

  • public:共享缓存可以保存响应;
  • private:响应只供私有缓存使用,通常不应进入 CDN 共享缓存;
  • max-age=N:响应在 N 秒内保持新鲜;
  • s-maxage=N:覆盖共享缓存的 max-age,不改变浏览器的有效期;
  • no-cache:可以存储,但每次复用前必须验证;
  • no-store:不要保存响应;
  • immutable:新鲜期内不需要因为刷新动作而重新验证。

no-cache 不是“不缓存”。HTML 使用 no-cache 时,浏览器可以保留正文,再通过 ETag 或 Last-Modified 进行条件请求;内容未变化时,服务器返回 304,避免再次传输完整页面。

ETag 负责确认内容有没有变化

服务端返回:

Cache-Control: no-cache
ETag: "page-a81d"

浏览器再次请求时可以发送:

If-None-Match: "page-a81d"

内容未变时返回 304 Not Modified,变化时返回新的 200 响应和 ETag。ETag 不能替代缓存策略;没有 Cache-Control 时,不同浏览器、代理和 CDN 仍可能采用不同的启发式缓存行为。

MDN 的 HTTP caching 指南(在新标签页打开)建议对可版本化的静态资源使用 cache busting,也就是把内容变化反映到 URL 中。这样发布和回滚都不依赖缓存清理速度。

文件指纹把更新变成新地址

按下面的顺序发布:

  1. 构建 app.<hash>.cssapp.<hash>.js 和带版本的字体文件;
  2. 上传新资源,但保留上一版本;
  3. 发布引用新 URL 的 HTML;
  4. 验证新 HTML、新资源和资源哈希;
  5. 回滚窗口结束后再清理旧文件。

先删旧资源再发布 HTML,会让仍在缓存中的旧页面请求到 404;只覆盖同名文件,则会让边缘节点和浏览器长期持有旧内容。

CDN TTL 和浏览器 TTL 分开控制

CDN 可以遵循源站 Cache-Control,也可以在控制台覆盖 TTL。Google Cloud CDN 的缓存文档(在新标签页打开)同样把 cache mode、源站响应头和客户端 TTL 分开处理。

配置 CDN 和浏览器缓存时,要分别确定:

  • 浏览器可以复用多久;
  • CDN 边缘节点可以复用多久;
  • 边缘过期后是重新验证,还是重新下载完整对象。

公开但更新频率较高的 JSON 可以使用:

Cache-Control: public, max-age=60, s-maxage=600
ETag: "catalog-20260902"

浏览器一分钟后重新验证,CDN 可在十分钟内服务同一对象。响应会因 Accept-Encoding、语言或来源变化时,还要配置正确的 Vary 或 CDN 缓存键;缓存键漏掉差异字段会把一个用户或一种格式的响应发给其他请求。

Caddy 按路径设置响应头

这组规则以 /assets/ 中的文件名带内容哈希为前提;如果构建系统不保证这一点,不要使用 immutable

example.com {
    root * /srv/site
    encode zstd gzip

    @immutable path /assets/*
    header @immutable Cache-Control "public, max-age=31536000, immutable"

    @html path / /index.html *.html
    header @html Cache-Control "no-cache"

    @media path /images/* /downloads/*
    header @media Cache-Control "public, max-age=86400"

    file_server
}

错误页不能因为路径匹配而继承一年的缓存。配置后必须同时测试 200、404 和跳转响应,确认错误状态的 TTL 符合预期。

连续发送 GET 验证缓存

HEAD 请求可能走不同规则。至少连续发送两次 GET,再比较响应头和正文:

url=https://cdn.example.com/assets/app.4f81c9a2.js

curl -sS -D /tmp/first.headers -o /tmp/first.body "${url}"
curl -sS -D /tmp/second.headers -o /tmp/second.body "${url}"

grep -Ei '^(cache-control|age|etag|last-modified|x-cache|cf-cache-status):' \
  /tmp/first.headers /tmp/second.headers
sha256sum /tmp/first.body /tmp/second.body

macOS 可把 sha256sum 换成 shasum -a 256。两次正文哈希应相同,Cache-Control 应包含预期指令,第二次请求应出现增长的 Age 或厂商 HIT 状态,ETag 也应与源站对象一致。

首次请求回源并写入边缘缓存,第二次请求命中缓存,同时核对响应头和正文哈希的流程图
HIT 只能证明命中了某个缓存对象;内容哈希或 ETag 才能确认它是不是当前版本。

更新后仍是旧内容时逐层定位

现象可能位置判断方法处理
只有当前浏览器显示旧文件浏览器或 Service Worker无痕窗口、Disable cache、检查 SW更新缓存策略或 SW 版本
不同地区看到不同版本CDN 边缘节点对比 Age、ETag、正文哈希刷新准确 URL,检查边缘规则
CDN 一直回源响应不可缓存或缓存键过细查看 Cache-Control、Cookie、Vary修正源站头和缓存键
刷新后很快又出现旧内容多源站版本不一致直连各源站比对哈希先统一源站制品,再清边缘缓存
新页面引用的资源 404发布顺序或旧资源已删除检查 HTML 中的实际 URL重新上传资源并延长旧版本保留期

缓存刷新适合处理误配或同名历史文件,正常发布仍应依靠不可变 URL。刷新 API 不能保证所有浏览器立即丢弃仍在新鲜期内的本地缓存。

缓存策略完成后,网站仍然慢时应把 DNS、连接、TTFB、下载和渲染分开测量,见网站打开慢怎么排查

常见问题

静态资源可以全部设置一年缓存吗?

不可以。只有内容变化后 URL 也会变化的资源,才适合一年缓存和 immutable。HTML 和同名更新文件需要短缓存或重新验证。

no-cache 和 no-store 有什么区别?

no-cache 允许保存,但复用前必须验证;no-store 要求不要保存。普通 HTML 常用前者,敏感响应使用后者。

有 ETag 还需要 Cache-Control 吗?

需要。Cache-Control 决定存储和新鲜度,ETag 只负责验证内容版本。

CDN 显示 HIT 就说明用户一定拿到新文件吗?

不一定。还要核对 URL、ETag 或正文哈希是否属于当前发布版本,并排除浏览器和 Service Worker 的旧缓存。

更新静态资源最可靠的方法是什么?

把内容哈希写入文件名,先上传新文件,再发布引用新 URL 的 HTML,并在回滚窗口结束前保留旧文件。