缓存头配错,表现不是页面打不开,而是"改了没生效"和"每次都重新下载"。今年 1 月我们给一个软件站做资源审计,四种错误全占了,最离谱的是 3.4MB 的静态资源每周产生 12 万次 304 请求。
错误一:HTML 和带 hash 的资源用同一套头
max-age=0, must-revalidate 配在 HTML 上是对的,配在 main.a1b2c3.js 上就是纯浪费。带 hash 的文件内容一变名字就变,可以直接 max-age=31536000, immutable。
这个站两种资源共用一套配置,结果每次访问都要对 40 多个静态文件做条件请求,移动端多花 300 毫秒。
错误二:改了文件没改 hash
反向的坑。文件名没变、内容变了、头又是长缓存,用户三个月看不到新版本。我们见过一个站改了 CSS 但没改 hash,客服被投诉两周"按钮错位"。
查法:对比 CDN 上的文件名和构建产物里的 hash。改法是把构建的 hash 和内容绑定做好,别用时间戳糊弄,时间戳一秒内构建两次就会撞。
错误三:CDN 上用了 private 或 no-store
有些框架默认给所有响应加 no-store,CDN 一看就不缓存,等于白买。这个站的营销页 HTML 就是 no-store,回源率 100%,TTFB 高得离谱。
改成 public, max-age=600 之后,回源请求从每天 6 万降到 8 千。这一步没改任何代码,只改了响应头。
错误四:多台机器各算 ETag
ETag 默认基于文件 inode 和修改时间生成。多台机器部署同一个文件,算出来的 ETag 各不相同,用户在不同节点间切换就会反复全量下载。
改法是在构建时统一生成基于内容哈希的 ETag,或者干脆去掉 ETag,靠 Cache-Control 和文件名版本控制。
一个附带问题:Service Worker 和缓存头打架
有些软件站为了做离线可用或者推送,注册了 Service Worker。它有自己的缓存层,会绕过浏览器的 HTTP 缓存策略。这时候你把 Cache-Control 改对了,用户还是看到旧版本,因为 SW 把旧资源扣在手里。
查法是 DevTools 的 Application 面板里看 Service Workers 和 Cache Storage,改完记得版本号加一。我们遇到过一个站,SW 缓存了三个月前的 JS,排查了两天才想到这一层。
怎么验证改对了
改完头别只看代码,用 curl 直接看线上响应:
curl -sI https://example.com/main.a1b2c3.js | grep -i -E "cache-control|etag|age"
age 这个头很有用,它是 CDN 缓存的时间戳,能直接看出命中了没有。命中时 age 会递增,回源时是 0。
十分钟检查清单
我们给客户的顺序是三件事:
- 打开首页,看 HTML 的
Cache-Control是不是public - 看带 hash 的 JS 和 CSS 是不是
immutable长缓存 - 去 CDN 后台看缓存命中率,低于 80% 就有问题
三项查完十分钟,比读代码见效快。缓存这类问题有个特点:配置错误一次,之后每个用户都在承担代价,而且没人投诉,因为大家以为"网站就这样"。
元析智能
为软件公司做 SEO 和 GEO 优化