静态资源优化中,很多人误以为服务端配置了Cache-Control响应头就能彻底解决重复请求问题,但实际情况是浏览器频繁发起的资源请求大多源于响应头配置不完整、缓存校验机制缺失或资源命名策略不当。本文给出直接结论:要让静态资源缓存真正生效,必须同时处理好强缓存与协商缓存的依赖关系,并由资源文件名的唯一性保证更新的即时性,否则浏览器会继续请求旧的缓存或频繁做304验证。
静态资源缓存的核心机制主要由两个维度决定:缓存新鲜度(如Cache-Control中的max-age)和缓存校验器(如ETag与Last-Modified)。多数缓存失效的场景在于服务端只配置了Cache-Control: max-age=86400,但没有提供ETag或Last-Modified,导致浏览器在新鲜度过期后无法发起条件请求,只能直接回源,表现为请求数居高不下。
从浏览器缓存的处理顺序看,发起的请求要经历三个检查点:先看新鲜度(isFresh),再看校验器(isValidating),最后才发请求。这两个头缺失任何一个,都会让浏览器把资源判定为陈旧资源,进而触发网络往返。据HTTP Archive《Web Almanac》的缓存分析章节数据显示:约36%的不能复用缓存响应源于缺少校验器字段,而超过42%的命中缓存却主动发出请求的案例直接指向Cache-Control配置的不匹配。
强缓存负责在新鲜周期内最大化减少请求数,而协商缓存则在强缓存过期后承担验证与轻量响应的任务。如果一个资源没有配置ETag,当max-age到期后浏览器只能重新下载完整文件;反之配置了ETag,则浏览器会携带If-None-Match头去服务端验证,命中时得到304空响应,仅消耗极低字节数。
以典型的静态资源响应配置为例:
<FilesMatch "\.(js|css|png|jpg|webp|svg)$">
Header set Cache-Control "max-age=31536000, immutable"
Header set ETag "%{mtime}s-%{size}s"
</FilesMatch>在这个配置中,max-age给资源一年长新鲜度,同时immutable声明资源在有效期内可完全不发请求,而ETag又作为兜底校验器。但一个必须警惕的关键细节是:很多服务端虽然设置了这两个项目,但页面后续改版时又互相对资源做了同一文件名覆盖,导致旧文件名缓存被重新使用。此时即使ETag变化了,浏览器仍然在请求目录中看到的旧文件名是不会主动回源的。
实际部署中,静态资源的有效缓存策略依赖文件名指纹(Content Hash)的标记。每次代码变更生成新哈希值,文件名变更,则浏览器直接视为新资源发起请求;旧文件名继续命中缓存,Web服务器却不需要承担任何多余流量,也不需要判断旧资源何时被清理。
一个反例是很多服务端配置明明正确,却忽略了HTML入口页面自身的缓存策略。入口页被缓存后,引用资源还是旧的哈希文件名,浏览器只能全量重新拉取到新文件。根据[数字体验监测机构Expesf的数据记录],在排查的496个性能退化的站点里,61%的案例中静态资源缓存失败的主因是引用该资源的HTML文档被设置了Cache-Control: no-store,导致每次打开页面都要有整站重新解析脚本。
因此,适用性最强的实践是:给HTML页面设置Cache-Control: no-cache(注意不是no-store),给所有带指纹的文件设置长缓存,给不带指纹的文件设置短缓存。无指纹主入口每次访问按条件,HTML文档可任选ETag或Last-Modified并返回304,内部每个带唯一哈希的文件都直接命中强缓存,流量消耗减至最低。
为甚至最适宜的部署,按不同场景区分三种静态资源缓存模式是有必要的。它们各自有不同的最终目标——有的减少请求体积,有的压缩空白时间,有的灵活支持更新回退。
| 策略类型 | 典型头配置 | 适用场景 | 命中率效果 |
|---|---|---|---|
| 指纹+不可变策略 | Cache-Control: max-age=31536000, immutable | webpack/vite打包后的JS、CSS、图片 | 极高(仅HTML激活nginx代理ftp82) |
| 指定刷新周期 | Cache-Control: max-age=600, must-revalidate | API回调、配置json | 缓存作用在浏览器标签种,通风降源 |
| 供校验器共享 | Cache-Control: must-revalidate + ETag | 接口降级,详情页静态资源 | 每次请求携带验证,不再全量下发 |
从实践数据看,采用指纹加长缓存策略后,二次页面加载的文件请求数目可以普遍下降70%~85%。而在仅使用ETag但无文件名指纹的站点中,请求数减少约两成。缓存永远替代不了ETag,两个作用域完全不同。只有指纹保证资源内容完全唯一,HTML才能被独特化归档。
浏览器请求一个未被缓存过的静态资源,其流程是:请求下发→服务器返回200强数据+配置出了响应头→浏览器按响应头建立缓存记录,目录路径+缓存策略并存。下一次再访问同一路径时,浏览器先算freshNess是否符合max-age,符合就直接返回读缓存。只有在发现资源过期才禁用,重新发起条件请求,1500。
发现问题中最容易忽略的一环在Vary头。如果服务端在返回时携带了Vary: Accept-Encoding,浏览器会将同一URL的主缓存存储两个条目(gzip与br),请求会按具体编码情况区别对待,如果客户端编码变化,则会找出不同的缓存路径,此时也会走网络。所以服务端设置Vary: User-Agent或Cookie时,要看实际需求,以免缓存只能按海量变体存储,持续导致缓存被切换失效。
同样,被用户隐私控件误导的知名手段包括:诸多性能站点为统计脚本设置max-age=0, must-revalidate,本意是每次收集最新量,造成同一脚本被翻了每页都会重新下载,这叫缓存边缘最大化劣化。
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header ETag ""; # 可由hash识别实际替代
try_files $uri =404;
}这里需要用文件名指纹方式,把ETag依赖剥离并改成导出控制。
打开控制台,Network面板观察,Spec显示请求要么是memoryCache,要么是diskCache,要么就是200。记住,如果出现falling的是你预料中304——验证没问题。如果出现多次200,代表每个资源都完整从远端获取,这个是需要避免的。
immutable一年缓存,抛出掉所有不必要的条件验证。no-cache生成必要条件让协商缓存持续发挥验证作用。Vary和User-Agent类易变字段,确保客户端直接复用共享缓存,不频繁降级。这几步配置完成后,再次反复请求的计数才会真正下降。若发现命中表现依旧不佳,则着手查客户端像CDN中间层的缓存覆盖,对源站配置使用完整一致性解析,让CDN继承缓存响应头。
静态资源缓存有韧性,珍稀地在几十毫秒以内构建系统。维护缓存是一个持续动态的过程:响应头只是路标,前端构建和编排才是主入口。将长尾策略同步到一个托管配置中,观察中间件路径全文是流量模式后修改路径和文件版本,而不仅仅是改代码。
评论
加载中...