前几天看了一下云服务商的账单,直接倒吸一口凉气。上个月刚上线的一个静态文档站,日活人数虽然涨了点,但 CDN 流量费用居然翻了十几倍。
赶紧去排查了一下 Nginx 访问日志 and CDN 控制台,发现了一个极其弱智但非常普遍的问题:用户每次刷新页面(哪怕只是随手的 F5 或者 Cmd + R),浏览器都在疯狂发送请求。虽然大部分响应都是 304 Not Modified,但每一个 304 请求都要走一遍「浏览器 -> CDN 边缘节点 -> 甚至回源」的协商缓存校验流程。
十万级甚至百万级的请求累加起来,不仅占用了大量的 CDN 连接数和带宽,还在用户端造成了明显的卡顿——毕竟协商缓存校验也是有网络开销的,在弱网环境下,几百个 304 请求排队能把页面卡死。
为什么传统的 max-age 拦截不了用户刷新?
很多人(包括以前的我)觉得,只要给静态文件设置了 Cache-Control: max-age=31536000,浏览器就会在一年内老老实实从本地缓存(disk cache / memory cache)读取,不再发起任何网络请求。
然而,这低估了浏览器「善解人意」的程度。
当用户主动点击浏览器的刷新按钮时,浏览器的行为模式会发生改变。以 Chrome 为例,普通刷新(F5)会强制对所有资源发起条件请求(Conditional Requests),即带上 If-None-Match (ETag) 或 If-Modified-Since。
这就意味着,哪怕你的 max-age 设置了一万年,只要用户按了刷新,浏览器还是会屁颠屁颠地去问服务器:“这文件变了没?”
这在以前静态文件名固定(比如 main.js)的时代是合理的,因为服务器可能更新了内容。但现在,现代前端打包工具(Webpack、Vite、Rspack)输出的静态资源全部带有文件哈希值(如 index-a8d2c3f9.js)。
只要哈希值不变,内容就绝对没有变;如果内容变了,哈希值也一定会变。
既然如此,为什么还要让浏览器大费周章地回源校验呢?
救星:immutable 属性
为了解决这个痛点,HTTP 规范引入了 immutable 指令(RFC 8246)。
当你在 Cache-Control 中声明 immutable 后,就是在明确告诉浏览器:“这个文件这辈子都不会变了。哪怕用户按了刷新键,你也别来烦我,直接从本地缓存里拿!”
目前主流浏览器(Chrome、Safari、Firefox)都已经对 immutable 提供了完美支持。配置了它之后,用户即使狂按 F5,在 Network 面板里也能看到这些静态资源直接显示为 from cache,彻底省去了与服务器通信的时间。
生产环境的 Nginx 最佳配置
在实际生产中,我们不能一股脑把所有资源都配置成 immutable。
- HTML 文件:绝对不能缓存,或者只能协商缓存。否则一旦发版,用户连新版网页的入口都拿不到。
- 带有 Hash 值的静态资源(JS、CSS、图片、字体):疯狂强缓存 +
immutable。 - 固定命名的静态资源(如
favicon.ico,或者某些不需要哈希的第三方包):建议常规强缓存,但不加immutable,或者设置较短的过期时间。
以下是我优化后的 Nginx 配置文件片段,完美解决了这个问题:
server {
listen 80;
server_name site-zh-it.example.com;
root /usr/share/nginx/html;
# 1. 针对入口 HTML 页面:绝对不能有强缓存,必须走协商缓存
location ~* \.(?:html|htm)$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
expires 0;
try_files $uri $uri/ /index.html;
}
# 2. 针对打包出来的哈希静态资源:直接拉满强缓存 + immutable
# 匹配诸如 main-a8d2c3f9.js, index.12345678.css 等带有 8-32 位哈希的文件
location ~* \.-[a-f0-9]{8,32}\.(?:js|css)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
}
# 兼容有些打包工具生成的格式:[name].[hash].[ext]
location ~* \.[a-f0-9]{8,32}\.(?:js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
}
# 3. 针对没有哈希值、或者固定名称的公共静态资源(如 favicon.ico)
# 给予较短的强缓存,但不配置 immutable
location ~* \.(?:ico|png|jpg|jpeg|gif|svg)$ {
add_header Cache-Control "public, max-age=86400";
access_log off;
}
}
踩坑提示:CDN 的 Cache-Control 传递
配置完 Nginx 后,如果你前面挂了 CDN(比如阿里云 CDN、Cloudflare 等),还需要去 CDN 控制台确认一件事:CDN 缓存配置是否尊重源站的 Cache-Control 头部。
默认情况下,很多 CDN 会为了提高命中率,强行覆写源站的缓存控制。你需要将 CDN 配置修改为「遵守源站规则(Respect Origin Headers)」,否则你辛苦配置的 immutable 在客户端根本不会生效。
搞定这一套后,观察了一周,CDN 下行流量请求数骤降 80% 以上,首屏秒开率也提升了,钱包终于保住了。
你们团队在发版时,是如何处理静态资源的版本管理和 CDN 缓存刷新的?有没有遇到过因 CDN 强缓存导致老用户访问到脏数据的大坑?欢迎在评论区分享你的排查经历。