Nginx 静态资源缓存策略实战:Cache-Control、ETag 与 expires 配置详解

网站里的图片、CSS、JavaScript 这些静态资源,占了一个页面绝大部分的字节数。一个文章页的 HTML 可能只有几十 KB,但配上一张 200KB 的图、三四个 CSS 和 JS 文件,整体轻松超过 500KB。如果不做缓存,用户每次刷新都要重新下载全部资源,不仅页面打开慢,还白白消耗服务器的带宽——对个人站长来说,流量也是一笔实打实的成本。浏览器缓存的作用,就是让这些不变的文件"只下载一次"。这篇文章讲清楚 HTTP 缓存的两种机制,以及 Nginx 里最实用的配置写法。

一、强缓存与协商缓存:两种缓存机制

浏览器缓存分两级。第一级叫强缓存:缓存有效期内浏览器直接用本地副本,根本不会发请求,在开发者工具里表现为请求显示 from memory cache 或 from disk cache。强缓存由响应头 Cache-Control 控制,最常用的是 max-age,单位是秒,比如 Cache-Control: public, max-age=604800 表示七天内直接复用。老协议里的 Expires 头(具体日期)功能相同但已经不再推荐,Nginx 的 expires 指令配置时会自动同时生成两个头。第二级叫协商缓存:缓存过期后,浏览器带着条件发请求问服务器"这个文件变了吗",没变服务器返回 304 Not Modified,浏览器继续用本地副本,传输的只是一个几十字节的响应头,代价极小。协商缓存由 ETag(内容指纹)和 Last-Modified(最后修改时间)两个头控制,Nginx 默认对静态文件开启,基本不用额外配置。理想的策略是:内容带版本号的资源用强缓存、缓存期给足,内容会变化的 HTML 用协商缓存或者很短的强缓存。

二、Nginx expires 指令的用法

Nginx 里最常用的缓存配置是指令 expires,它放在 location 块里,作用是给响应自动添加 Cache-Control 和 Expires 两个头。写法很简单:expires 7d; 表示缓存七天;expires max; 表示最长缓存(十年);expires -1; 表示不缓存,每次都走协商。个人博客最实用的做法是按资源类型分别设置:

location ~* \.(jpg|jpeg|png|gif|webp|svg|ico)$ {
    expires 30d;
    add_header Cache-Control "public, no-transform";
}
location ~* \.(css|js)$ {
    expires 7d;
}
location / {
    expires -1;
}

图片、图标这类几乎不变化的资源缓存三十天到一年,CSS 和 JS 缓存一周到一个月,HTML(也就是 location / 匹配的动态页面)不缓存或者只缓存很短时间,因为文章内容随时会更新。

三、版本号与文件指纹:解决"更新不生效"

静态缓存最大的坑是"更新不生效":CSS 缓存了七天,你改了样式,用户七天内看到的还是旧版本。解法是"文件名指纹":构建时给文件名加上内容哈希,比如 style.a3f9c2.css,内容一变哈希就变、文件名就变,浏览器把它当成全新文件重新下载;而旧文件永远不会再被引用,可以放心地用最长缓存。对不使用构建工具的 Typecho 博客,退而求其次的做法是在引用 URL 上加版本参数:style.css?v=20260822,升级时改一下参数值。要注意,加参数只是触发浏览器重新下载的手段,真正的缓存时长仍然由服务器响应头控制,两者配合才完整。还有一条铁律:首页、栏目页、文章页这类 HTML 一定不要长缓存,否则用户看到的永远是旧页面,内容更新"看不见",对个人博客是灾难。

四、gzip 压缩配合

缓存解决的是"重复下载",压缩解决的是"下载体积"。Nginx 开启 gzip 对文本类资源收益极大,CSS 和 JS 通常能压缩掉百分之六十到八十。配置方法:

gzip on;
gzip_min_length 1k;
gzip_comp_level 6;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;

图片(jpg、png、webp)本身已经是压缩格式,不要再开 gzip 浪费 CPU。字体和 SVG 这类资源要保证 Content-Type 正确,否则 gzip_types 匹配不上、压缩不生效。开启之后可以用 curl -H "Accept-Encoding: gzip" -I 请求一个 CSS 文件,看响应头里有没有 Content-Encoding: gzip 来确认。

五、与 CDN 缓存配合

网站套了 CDN 之后,缓存分两层:CDN 边缘节点缓存一层,浏览器缓存一层。CDN 会遵循源站返回的 Cache-Control 头决定边缘缓存多久,所以源站 Nginx 的缓存头写对了,CDN 的行为就跟着对了。如果想让 CDN 缓存时间长一点、浏览器缓存短一点,可以在 CDN 平台单独配置边缘缓存规则,或者用 s-maxage 这类专门给 CDN 看的指令。还有一个常见问题:CDN 和浏览器都缓存之后,文章更新可能出现"CDN 上还是旧页面"的情况,解决办法是发布文章后在 CDN 平台对对应 URL 做刷新或预取,国内主流 CDN 都有这个功能;更推荐的做法是配置规则让 HTML 不缓存,只缓存带指纹的静态资源,这也是行业里最稳妥的方案。

六、验证与排错

配置完怎么确认生效?用 curl -I 请求一个静态资源看响应头:有 Cache-Control: max-age 和 Expires 说明强缓存生效;有 ETag 和 Last-Modified 说明协商缓存可用;同一个 URL 请求第二次返回 304,说明协商缓存工作正常。浏览器开发者工具的 Network 面板更直观:Size 列显示 from disk cache 或 from memory cache 就是强缓存命中,状态列显示 304 就是协商缓存命中。常见问题有两个:一是配置了 expires 但响应头里没有,多半是 location 正则没匹配上,检查匹配写法和文件实际路径;二是 add_header 互相覆盖,Nginx 里同一层级多个 add_header 指令后面的会覆盖前面的,需要写全。改完配置记得先执行 nginx -t 检查语法,再 reload 生效。

七、几个容易忽略的细节

第一个细节是浏览器行为差异:用户在地址栏按回车是普通刷新,会走协商缓存;按 F5 强制刷新会绕过强缓存、直接发请求;按 Ctrl+F5 连协商缓存都跳过,强制重新下载。所以测试缓存配置时,不要用 F5 的结果来判断强缓存是否生效,要看第一次访问的表现,或者用开发者工具勾选 Disable cache 之后再测试。第二个细节是隐私相关的资源:如果站里有登录后可见的私人页面、个人中心接口,这些响应不要用 public 缓存,public 意味着任何中间层(CDN、代理)都可以缓存,正确写法是 Cache-Control: private 或者干脆不缓存,避免把用户数据缓存到共享节点上。第三个细节是 no-cache 和 no-store 的区别:no-cache 是"可以用但必须先问服务器",等价于每次都走协商;no-store 才是真正禁止存储,配置时不要把两者混用。

第四个细节是动态页面的缓存问题。文章页虽然是 PHP 动态生成的,但内容在两次更新之间其实并不变化。追求极致性能的站长会给动态页面也加一层缓存:要么用 Typecho 的页面缓存插件,要么用 Nginx 的 fastcgi_cache 把 PHP 响应缓存几十秒到几分钟。fastcgi_cache 配置稍复杂,要处理好缓存键和清缓存时机,适合有点经验的站长尝试;新手阶段把静态资源缓存和 gzip 做好,收益已经足够明显。等访问量上来了,再考虑动态缓存也不迟。

第五个细节是移动端与自适应资源。如果站点为手机和电脑提供了不同尺寸的图片,或者用了响应式图片,浏览器缓存会按完整 URL 区分,不会互相干扰,但记得给不同尺寸的图片都配上一致的缓存头。另外 favicon 这类小文件容易被忽略,它虽然只有几 KB,但几乎每个页面都会被请求,给图标设置 expires 30d 能减少大量无效请求,也算举手之劳。

总结:静态资源缓存是性价比最高的网站性能优化之一,一次配置,带宽和加载速度同时受益。核心就三句话:带指纹的资源往长了缓存(一年起步),HTML 不缓存,图片和字体缓存一个月以上;再用 gzip 压缩文本资源;套 CDN 时保持源站缓存头正确。配置完用 curl -I 验证一下响应头,这套方案对个人博客来说既简单又稳定。

Last modification:August 22nd, 2026 at 08:09 am

Leave a Comment