5个站点挨个配Nginx缓存花了2小时,30个站用include统一模板加版本号策略5分钟搞定,页面加载速度还快了47%
做站群的人早晚会遇到这个问题:网站数量从5个涨到30个,某天发现首页加载要3秒多,Google PageSpeed Insights一片红。然后你打开Nginx配置文件,发现最早的几个站配了expires和gzip,中间一批站只配了gzip没配缓存头,最新的几个站什么缓存配置都没有。补一个站大概20分钟,30个站就是10小时,中间还可能改错一行导致整个站502。
这不是个例。在Google搜索控制台里,页面体验那一栏的Core Web Vitals数据,跟缓存配置好坏有直接关系。一个LCP(最大内容绘制)超过2.5秒的页面,哪怕内容写得再好,排名也很难上去。今天把多站点缓存批量配置这件事从头到尾拆开:缓存策略怎么选、按文件类型怎么分、30个站怎么统一模板化、更新资源怎么让缓存即时失效、CDN缓存和浏览器缓存怎么协同。
批量配置缓存的五个关键决策
| 1 | 按文件类型分缓存时长,不是按站点分。图片设1年、CSS/JS设30天、HTML设不缓存或5分钟——这个粒度是跨站点通用的,不需要每个站单独设计。 |
| 2 | 用include统一模板,不要在30个server块里重复写。一个conf文件管所有站,改一处30个站全部生效。 |
| 3 | 版本号策略解决"更新了CSS用户还看旧版"的问题。文件名加hash(style.a3f2b.css),而不是靠改缓存时长。 |
| 4 | 浏览器缓存和CDN缓存是两层,要分开配。浏览器缓存管用户本地,CDN缓存管边缘节点回源,配置逻辑不同。 |
| 5 | 配完之后必须验证。浏览器DevTools、curl -I、在线检测工具三道关卡都要过一遍,不能"配了就完事"。 |
一、缓存该管什么:浏览器缓存、服务器缓存、CDN缓存三层各管各的

很多人一说"配缓存",第一反应就是在Nginx里加一行expires 30d;。这其实只配了浏览器缓存这一个层面。一个完整的缓存体系至少有三层,每层失效机制不一样,配置方式也不同。
| 缓存层级 | 存储位置 | 控制方式 | 适用场景 | 失效特点 |
|---|---|---|---|---|
| 浏览器缓存 | 用户本地磁盘 | Cache-Control、Expires头 | 图片、CSS、JS、字体 | 按时间过期,用户清缓存才会强制刷新 |
| 服务器缓存 | Nginx fastcgi_cache / proxy_cache | proxy_cache_path等指令 | 动态页面(PHP生成) | 按时间+访问频率双重控制 |
| CDN缓存 | CDN边缘节点 | CDN控制台+源站Cache-Control头 | 全部静态资源+可缓存HTML | 按CDN缓存规则+手动刷新 |
三层之间有先后顺序。用户访问一个页面时,先看浏览器本地有没有、没过期就用;本地没有或过期了,请求发到CDN节点;CDN节点有且没过期就直接返回;CDN也没有才回源到你的Nginx服务器。如果源站Nginx还配了proxy_cache,那Nginx也不会每次去后端PHP动态生成,而是拿缓存结果直接返回。
容易踩的坑:配了CDN缓存就以为万事大吉,结果浏览器缓存头没配好,用户本地一直用旧版本,CDN刷新了也没用。浏览器缓存的优先级高于CDN缓存,两个都要配到位。
二、按文件类型分缓存时长,一张表就能说清楚
缓存策略的核心原则就一条:内容越"不可变"的,缓存时间越长;内容越可能变的,缓存时间越短或直接不缓存。文件名的稳定性直接决定了你能设多长的缓存。带版本hash的文件名(如logo.a3f2b.png)可以放心设一年;不带版本号的CSS文件设30天都有风险。
| 文件类型 | 推荐缓存时长 | Cache-Control值 | 为什么这么设 |
|---|---|---|---|
| 图片(png/jpg/webp/svg/ico) | 365天 | public, max-age=31536000, immutable | 图片内容极少变动,一旦上传基本就是终版,文件名改了就相当于新文件 |
| 字体(woff2/woff/ttf) | 365天 | public, max-age=31536000, immutable | 字体文件几乎不会改,设一年完全没问题 |
| CSS / JS(带hash版本号) | 365天 | public, max-age=31536000, immutable | 文件名带hash(如main.7f3a2b.js),内容变了文件名就变,浏览器自动请求新文件 |
| CSS / JS(不带hash) | 7-30天 | public, max-age=604800 | 不带版本号意味着改了内容文件名不变,缓存太长会导致用户看不到更新 |
| HTML页面 | 不缓存/5分钟 | no-cache 或 max-age=300 | HTML内容随时可能更新,搜索引擎蜘蛛来了要看到最新版 |
| JSON/API接口响应 | 不缓存 | no-store | 动态数据,缓存会导致数据不一致 |
这里有一个关键细节:immutable这个指令很多人没加。它的作用是告诉浏览器"这个资源在缓存有效期内绝对不会变",这样即使用户按了F5强制刷新,浏览器也不会发送带If-Modified-Since的条件请求去服务器验证。对于带hash的静态资源,加了immutable可以减少大量无意义的304响应。
一个实用的经验公式:如果你不确定某类文件该设多长缓存,问自己一个问题——"这个文件改了以后,用户需要多快看到新版本?"如果是CSS改了样式要立刻生效但你又没做版本号,那缓存不要超过1小时。如果改了图片可以接受延迟几天看到,那设30天甚至1年都可以。
三、Nginx配置怎么写:一个模板文件搞定所有站点
到了实操环节。30个站点,每个站点的server块里都写一遍expires和Cache-Control,改一次就要改30个地方。正确做法是把缓存配置抽成一个独立的conf文件,所有站点用include引用。
先创建缓存策略模板文件。在Nginx配置目录下新建/etc/nginx/conf.d/cache-policy.conf,把所有按文件类型的缓存规则集中写在这里:
# ============================================# 缓存策略统一模板 cache-policy.conf# 所有站点通过 include 引用,改一处全站生效# ============================================# 1. 图片文件:1年强缓存 + immutablelocation ~* \.(png|jpg|jpeg|webp|gif|ico|svg|bmp)$ {expires 365d;add_header Cache-Control "public, max-age=31536000, immutable";add_header Vary "Accept-Encoding";}# 2. 字体文件:1年强缓存location ~* \.(woff2|woff|ttf|eot|otf)$ {expires 365d;add_header Cache-Control "public, max-age=31536000, immutable";add_header Vary "Accept-Encoding";}# 3. 带hash的CSS/JS:1年强缓存 + immutable# 文件名示例:main.a3f2b91.js, style.7c8e2d4.csslocation ~* \.[a-f0-9]{7,}\.(css|js)$ {expires 365d;add_header Cache-Control "public, max-age=31536000, immutable";}# 4. 不带hash的CSS/JS:7天缓存(保守策略)location ~* \.(css|js)$ {expires 7d;add_header Cache-Control "public, max-age=604800";}# 5. 视频/音频:30天location ~* \.(mp4|webm|ogg|mp3|wav)$ {expires 30d;add_header Cache-Control "public, max-age=2592000";}# 6. PDF/文档:30天location ~* \.(pdf|doc|docx|xls|xlsx|ppt|pptx)$ {expires 30d;add_header Cache-Control "public, max-age=2592000";}# 7. JSON/XML数据:禁用缓存location ~* \.(json|xml)$ {expires -1;add_header Cache-Control "no-store, no-cache, must-revalidate";}# 8. HTML页面:不缓存,但允许ETag条件验证# 这个规则优先级最高,因为HTML的location通常在前面的server块里已经定义# 如果server块有独立的 location / 规则,需在那里单独配置然后在每个站点的server块里只需要一行:
server {listen 80;server_name site1.example.com;root /var/www/site1;# 这一行代替之前所有手写的expires和add_headerinclude /etc/nginx/conf.d/cache-policy.conf;# HTML页面单独配置——不缓存location / {try_files $uri $uri/ /index.php?$args;add_header Cache-Control "no-cache, must-revalidate";add_header Pragma "no-cache";}}include的优先级逻辑:Nginx中location的匹配顺序是"精确匹配 > 前缀匹配 > 正则匹配"。include进来的location正则规则会被逐一评估。如果你在server块里单独定义了location /(前缀匹配),它不会覆盖include里的正则location,两者各自生效。所以HTML的no-cache要在server块的location /里单独写,不需要在include模板里重复。
30个站的新增、修改只需要改cache-policy.conf一个文件,然后nginx -t && nginx -s reload,所有站点全部生效。从30个文件挨个改,变成改1个文件加1条命令。
四、版本号策略:CSS改了用户还看旧版,不是缓存的问题
"配了缓存之后CSS更新了,用户那边还是旧版样式"——这几乎是缓存配置中最常见的抱怨。但这个问题的根源不是缓存配置错了,而是文件名没变。
缓存策略的设计前提是:不可变资源的文件名必须是唯一的。一张logo.png你今天改了一版,如果文件名还叫logo.png,那浏览器就认为它还是之前那个文件,自然用本地缓存。解决办法就三个字——文件名加hash。
❌ 错误做法
改完style.css后,为了"让用户看到新版本",把缓存时间从30天改成1小时。每次更新都缩短缓存,最后缓存等于没配。

✅ 正确做法
构建工具自动给CSS/JS文件名加内容hash,输出style.a3f2b.css。内容变了hash就变,文件名就变,浏览器自动当新文件请求。
构建工具的配置也很简单。Webpack用[contenthash:8],Vite默认就带hash,Gulp用gulp-rev。WordPress站群如果不用构建工具,可以用插件在引入资源时加版本参数,比如style.css?ver=20260801。
| 站点类型 | 推荐版本号方案 | 可设缓存时长 |
|---|---|---|
| 前端构建项目(React/Vue) | Webpack/Vite自动hash文件名 | 365天 + immutable |
| 传统jQuery/Bootstrap站点 | 手动加版本号参数 ?v=20260801 | 30天 |
| WordPress站群 | wp_enqueue_style第四参数设版本号 | 365天(WP自带版本号机制) |
| 纯HTML静态站 | 构建时脚本批量rename加hash | 365天 + immutable |
还有一个很多人忽略的细节:HTML文件里引用的资源路径也要同步更新。光把CSS文件名从style.css改成style.a3f2b.css没用,HTML里的link标签href也得跟着改。这一步如果不自动化,30个站手动改一遍HTML引用,工作量比配缓存本身还大。所以要么用构建工具自动处理,要么在WP里用函数统一输出版本号。
UC建站系统的处理方式:UC系统底层是WP+AI管理层架构,主题资源引入自带WP的版本号机制,每个站点的CSS/JS引用URL自动带上主题版本号参数。改版时更新版本号常量,所有30个站点的资源引用一次性全部切换到新版本,不需要逐个站点改HTML。配合include统一缓存模板,缓存配置和版本控制两条线各司其职。
五、CDN缓存和浏览器缓存不是一回事,分开配才能不出问题
用了CDN(不管是CloudFlare、阿里云CDN还是腾讯云CDN)之后,缓存链路变成了:浏览器缓存 → CDN边缘节点缓存 → 源站。每一步都有独立的缓存策略,而且CDN的缓存规则优先级往往高于源站Nginx发出的Cache-Control头。
典型问题:你Nginx里配了HTML不缓存(no-cache),但CDN控制台里有一条规则"缓存所有.html文件30天",结果用户看到的一直是CDN节点上的旧版HTML。排查半天才发现Nginx的配置在CDN层被覆盖了。
常见踩坑:两边策略冲突
Nginx设了Cache-Control: no-cache,CDN控制台规则设了"全部缓存1天"。CDN覆盖了源站指令,HTML被错误缓存。
正确做法:源站为准,CDN跟随
CDN设为"遵循源站",所有缓存策略由Nginx的Cache-Control头决定。特殊规则(如目录级缓存)在CDN控制台单独配置并做好记录。
CDN缓存还有一个特有的"缓存Key"概念。CDN判断两个请求是否命中同一份缓存,靠的是缓存Key。默认的缓存Key通常包含"域名+URI",但不包含Query参数。这导致一个问题:style.css?v=1.0和style.css?v=2.0在CDN看来是同一个文件,因为URI都是/style.css。
| CDN配置项 | 推荐设置 | 原因 |
|---|---|---|
| 缓存策略模式 | 遵循源站(Cache-Control) | 避免CDN覆盖Nginx配置,以源站为准 |
| 缓存Key-是否包含Query参数 | 保留全部Query参数 | 否则?v=1和?v=2被当成同一个文件 |
| 目录级特殊规则 | HTML不缓存,静态资源按源站 | HTML页面的缓存必须关掉 |
| 缓存刷新方式 | 目录刷新 > URL刷新 > 全站刷新 | 全站刷新会导致瞬间大量回源,服务器扛不住 |
CDN缓存Key的坑:如果你用Query参数做版本控制(style.css?v=2.0),一定要确认CDN的缓存Key包含Query参数。阿里云CDN默认是"忽略参数"模式,需要手动改为"保留全部参数"或者"过滤指定参数"。否则新旧版本在CDN层互相覆盖,浏览器请求?v=2.0拿到的是CDN缓存的?v=1.0的旧文件。
六、Gzip/Brotli压缩是缓存的好搭档,但别把两者搞混
经常有人把"缓存"和"压缩"当成一回事来讨论。它们解决的问题完全不同:缓存解决的是"能不能不请求服务器",压缩解决的是"请求了能不能传得更快"。两者配合使用效果最好——压缩后的文件体积小,即使缓存没命中需要回源,传输时间也大幅缩短。
Nginx里Gzip的配置同样适合用include统一管理。在/etc/nginx/conf.d/gzip-policy.conf里写:
# ============================================# Gzip压缩统一模板 gzip-policy.conf# ============================================gzip on;gzip_vary on; # 告诉CDN按Accept-Encoding区分缓存gzip_min_length 1024; # 小于1KB的文件不压缩gzip_comp_level 6; # 压缩级别1-9,6是性价比最优gzip_proxied any; # 对所有代理请求生效gzip_typestext/plaintext/csstext/javascripttext/xmlapplication/javascriptapplication/jsonapplication/xmlapplication/rss+xmlimage/svg+xmlfont/ttffont/woff2; # woff2已经是压缩格式,不重复压缩# 禁用IE6的gzip(如果你的用户还有IE6...)gzip_disable "msie6";gzip_vary这个参数很容易漏掉。它的作用是给响应头加上Vary: Accept-Encoding,告诉CDN"同一个URL,浏览器支持gzip和浏览器不支持gzip,应该分别缓存两份"。如果不加这个头,CDN可能给不支持gzip的旧设备返回了压缩后的乱码内容,或者给支持gzip的设备返回了未压缩的大文件。
Brotli是比Gzip更新、压缩率更高的算法(同文件体积通常比Gzip小20%左右),但需要Nginx编译了brotli模块才能用。如果服务器支持,优先用Brotli,Gzip作为降级备选。配置方式类似,只是在gzip配置旁边再加一段brotli配置,同样用include统一引用。
七、配完不等于完事,三步验证少一步都可能白配

缓存配置最容易出问题的环节不是写配置,而是"以为配好了实际上没生效"。缓存不生效比没配缓存更麻烦——因为它会让你错误地以为"缓存没用",其实只是没配对。
验证一:curl -I 看响应头
curl -I https://你的域名/static/logo.png
检查返回的Cache-Control、Expires、ETag头是否符合预期。这是最快的验证方式。
验证二:浏览器DevTools Network
F12 → Network → 刷新页面。Size列显示"(disk cache)"或"(memory cache)"说明浏览器缓存生效。Status列是304说明走了条件验证但没走强缓存。
验证三:在线检测工具
Google PageSpeed Insights、GTmetrix、WebPageTest都会检查缓存策略。它们能一次性扫出所有没配缓存头的资源,比手动一个个检查高效。
30个站逐个人工验证显然不现实。写一个简单的批量检测脚本:
#!/bin/bash# 批量检测所有站点的缓存头配置SITES=("https://site1.example.com/static/logo.png""https://site2.example.com/css/main.css"# ... 更多站点)for url in "${SITES[@]}"; doecho "=== $url ==="curl -sI "$url" | grep -E "(Cache-Control|Expires|ETag)"echo ""done这个脚本跑一遍,30个站有没有缺缓存头、哪些站Cache-Control值不对,一目了然。比打开30个浏览器窗口挨个看DevTools快得多。
八、四个配置翻车现场,每个都有人踩过
翻车1:location匹配顺序搞反了
Nginx的location匹配优先级是:精确匹配(=) > 前缀匹配(^~) > 正则匹配(~ ~*) > 普通前缀匹配。如果你在server块里写了一个location / { }普通前缀匹配,又在include里写了location ~* \.(css|js)$ { }正则匹配——CSS/JS请求会命中正则规则,而不是被location /覆盖。所以include的缓存规则对CSS/JS是生效的,但很多人以为不生效就去改expires时间,越改越乱。
翻车2:ETag和Last-Modified没关导致304满天飞
你配了Cache-Control: max-age=31536000,但Nginx默认还会发ETag和Last-Modified头。浏览器即使强缓存没过期,如果用户按了F5(非Ctrl+F5),浏览器可能发条件请求验证,服务器返回304。304虽然不传body,但一次往返也要几十到几百毫秒。对带hash的静态资源,加add_header Cache-Control "immutable"可以避免F5触发条件请求。
翻车3:include路径写错了Nginx没报错
include /etc/nginx/conf.d/cache-policy.conf;——如果这个文件不存在或路径写错了,nginx -t会直接报错。但如果是用通配符include /etc/nginx/conf.d/*.conf;,没有匹配到文件也不报错,静默失败。批量配置建议用绝对路径+精确文件名,不用通配符。配完之后nginx -T | grep cache-control确认规则确实加载进去了。
翻车4:CDN回源时带了错误的Host头
CDN回源时发给Nginx的Host头可能是CDN节点的域名而不是你的源站域名。Nginx如果按server_name匹配不到对应server块,就会走到默认server块(第一个或default_server),那个块里可能根本没有缓存配置。解决方法是源站Nginx加一个catch-all的server块,里面至少include基础缓存策略,或者在CDN控制台配置回源Host头为正确的源站域名。
九、从手配到系统化:站群缓存的终极形态
前面讲的是"配置层面"的批量管理——用include统一模板、按文件类型分时长、版本号策略、CDN协同。但如果你管的不只是30个站,而是100个、200个,甚至站点还在持续新增,那光靠include还不够。
更深一层的问题是:新站点创建时,缓存配置是自动就位的还是需要手动去加一行include?站点模板更新(比如新增了一种文件类型的缓存规则),是手动改cache-policy.conf还是系统自动同步?站点下线后,CDN上对应域名的缓存规则要不要清理?
| 管理方式 | 10个站 | 30个站 | 100+个站 |
|---|---|---|---|
| 手动逐个配置 | 勉强可行,2小时 | 很痛苦,半天起步 | 不可行,必出错 |
| include统一模板 | 10分钟 | 10分钟 | 可以,但新建站要手动加include |
| 建站模板内置缓存 | 零手工 | 零手工 | 零手工,新建站自动就位 |
| Ansible/自动化脚本批量推送 | 零手工 | 零手工 | 零手工,多服务器统一管理 |
对于30个站以内,include统一模板已经足够。对于100+站并且分布在多台服务器上的场景,就需要建站模板内置缓存配置+Ansible批量推送的组合了。
UC建站系统在这个问题上的处理逻辑:每个新站点创建时,Nginx server块的配置模板自动包含缓存策略include和Gzip压缩include,不需要手动加。所有站点的缓存头、压缩参数、安全头等配置在系统层面统一管理,改一个配置项所有站点同步生效。CDN接入也是在系统层面统一配置回源规则和缓存Key策略,避免了一个站一个站去CDN控制台手动设置的重复劳动。
缓存这件事,单个站配置对了能省30%-50%的服务器带宽和加载时间。但真正的收益在规模化以后才显现——30个站、100个站,每个站省50%带宽,加起来省下的服务器成本够多养两台机器。更重要的是页面速度提升带来的排名红利:Google把Core Web Vitals纳入排名信号之后,LCP从3秒优化到1.5秒,不是"用户感觉快了一点"的问题,是实实在在影响自然流量的因素。
最后说一句,缓存配置不是"配一次就永远不用管"的事情。站点改版、新增CDN、换服务器、升级HTTPS——每次基础设施变动之后,都值得用curl -I把关键资源的响应头再过一遍。很多"网站突然变慢了"的问题,排查到最后就是某次改配置把缓存头改丢了。
