静态站的缓存配置容易走两个极端:要么全都不缓存,白白浪费带宽;要么一刀切全缓存,改完线上半天不生效。
正确做法是按文件名是否带内容 hash 来分。
两类资源,两种策略
现代构建工具的产物大致分两类:
带 hash 的资源 —— index-a3f2c1.css、chunk-8d4e.js。文件名里的 hash 由内容算出,内容一变文件名就变。这类文件可以放心缓存到天荒地老,因为它永远不会被”就地更新”。
入口 HTML —— index.html。它的 URL 是固定的,内容却随时可能变。这类文件必须每次回源校验。
location /_astro/ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
location ~* \.html$ {
add_header Cache-Control "no-cache, must-revalidate";
}
immutable 这个指令值得单独说:它告诉浏览器”这个文件永远不会变,用户按刷新键也别来问我”。没有它,用户刷新页面时浏览器仍会对每个资源发一次带 If-None-Match 的请求,拿一堆 304 回来——省了带宽,没省往返延迟。
no-cache 不是不缓存
这是个长期被误读的名字。
no-cache—— 可以存,但每次使用前必须回源校验(通常靠 ETag 换一个 304)no-store—— 一个字节都不许存
对 HTML 你要的是前者。资源仍然缓存在本地,只是每次多一个轻量的校验请求;命中 304 时不传输正文,成本很低。用 no-store 会强制每次完整下载,没有必要。
HTML 为什么值得单独对待
HTML 不缓存不只是工程洁癖,它决定了你「改完多久能生效」。
页面标题、页脚文字、导航结构这些东西都只存在于 HTML 里。万一发现某处写错了一个字,或者需要紧急下线某个链接,你希望改完部署就生效——而不是等某个 CDN 节点的 TTL 慢慢过期,或者用户浏览器里还压着上周的副本。
带 hash 的资源缓存一年没有任何风险,因为改动必然带来新文件名。HTML 的 URL 是固定的,所以必须实时。这条边界画得很清楚。
顺手做的两件事
压缩。文本类资源开 gzip 就够用,gzip_comp_level 6 是压缩率和 CPU 的常见折中点。追求极致可以用 Brotli,需要额外编译模块,静态站也可以预压缩成 .br 文件直接分发。
gzip on;
gzip_vary on;
gzip_comp_level 6;
gzip_min_length 1024;
gzip_types text/css application/javascript application/json image/svg+xml;
注意 gzip_types 里不用写 text/html——Nginx 总是压缩它,写了反而会有告警。
关掉静态资源的访问日志。一个页面几十个资源请求,日志里全是噪音,真出问题时反而不好找。access_log off 放在资源的 location 里,HTML 的日志留着。
验证
改完配置别只看浏览器。用 curl 直接看响应头:
curl -sI https://example.com/ | grep -i cache-control
curl -sI https://example.com/_astro/index.a3f2c1.css | grep -i cache-control
第一条应该是 no-cache, must-revalidate,第二条应该是 public, immutable。两条都对,这套策略才算落地。