静态资源不能使用一条统一缓存规则。带内容指纹的 CSS、JavaScript 和字体可以长期缓存;HTML 要能较快发现新版本;未改名的图片只能在可接受的旧内容窗口内缓存;包含账号或支付信息的响应不应进入共享缓存。
缓存策略先看 URL 是否随内容变化
| 资源 | URL 变化规则 | 常用策略 | 发布要求 |
|---|---|---|---|
| HTML | 地址长期不变,内容会更新 | no-cache 或较短 max-age | 能及时重新验证或过期 |
| 带内容指纹的 CSS/JS/字体 | 内容变化时文件名变化 | public, max-age=31536000, immutable | HTML 必须引用新 URL |
| 未带版本的图片、PDF | 同一地址可能替换 | 短缓存加 ETag | 更新后刷新 CDN 或更换 URL |
| 公开 API 响应 | 由业务语义决定 | 明确 public/private、s-maxage 和 Vary | 缓存键覆盖所有响应差异 |
| 敏感或一次性响应 | 不允许被复用 | no-store | CDN 同样不得缓存 |
app.4f81c9a2.js 的内容变化后会生成新文件名,旧 URL 才能被视为不可变。只有目录叫 /assets/,并不能证明里面每个文件都不会原地更新。
Cache-Control 决定谁能存、何时复用
RFC 9111(在新标签页打开)定义了 HTTP 缓存语义;immutable 是 RFC 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 中。这样发布和回滚都不依赖缓存清理速度。
文件指纹把更新变成新地址
按下面的顺序发布:
- 构建
app.<hash>.css、app.<hash>.js和带版本的字体文件; - 上传新资源,但保留上一版本;
- 发布引用新 URL 的 HTML;
- 验证新 HTML、新资源和资源哈希;
- 回滚窗口结束后再清理旧文件。
先删旧资源再发布 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 也应与源站对象一致。
更新后仍是旧内容时逐层定位
| 现象 | 可能位置 | 判断方法 | 处理 |
|---|---|---|---|
| 只有当前浏览器显示旧文件 | 浏览器或 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,并在回滚窗口结束前保留旧文件。