很多人在优化网站性能时都会开启gzip压缩,它能显著减少传输体积,加快页面加载速度。比如一个100KB的JS文件,启用gzip后可能压缩到30KB左右,节省了70%的带宽。但很少有人注意到,这种压缩方式其实也悄悄影响着缓存的命中情况。
压缩前后内容不同,缓存怎么认?
浏览器和CDN缓存系统通常是根据请求的URL和部分头部信息来判断是否命中缓存。当你开启gzip后,服务器会根据客户端是否支持(通过Accept-Encoding头判断),决定返回压缩版还是原始版本。这意味着同一个资源可能会有两种不同的响应体:压缩过的和未压缩的。
如果缓存系统没有正确区分这两种情况,就可能出现问题。比如用户A用支持gzip的浏览器访问了资源,缓存里存的是压缩后的版本;用户B用了一个老设备,不支持gzip,结果却从缓存拿到了压缩数据,浏览器无法解压,页面直接报错。
缓存键的设计很关键
为了避免这种情况,现代缓存系统通常会把请求头中的Accept-Encoding也纳入缓存键的计算范围。也就是说,/index.js 对应两个缓存项:一个是 Accept-Encoding: gzip 的压缩版本,另一个是空或不包含gzip的原始版本。这样就能保证不同客户端拿到各自能处理的内容。
但这也会带来副作用——缓存碎片化。原本一个资源只需要缓存一次,现在要缓存多个变体,整体命中率反而可能下降。尤其是在用户设备类型复杂、HTTP客户端五花八门的情况下,缓存效率会被拉低。
实际场景中的权衡
举个例子,某新闻网站在凌晨高峰期发现CDN缓存命中率突然掉了5个百分点。排查后发现,是因为新上线的监控脚本在部分节点上没触发gzip压缩,导致同样的资源生成了非压缩副本,缓存分散了。后来统一配置了压缩策略,并确保Vary头正确设置,才恢复稳定。
说到Vary头,这是解决这类问题的重要手段。服务器可以在响应中加上:
Vary: Accept-Encoding
这相当于告诉缓存系统:“这个资源的输出依赖于Accept-Encoding头,请把这个头的值考虑进缓存键。”这样一来,无论是nginx、Apache还是Cloudflare这类CDN,都能更准确地管理多版本缓存。
移动端尤其需要注意
一些老旧安卓机上的WebView可能不规范地声明编码支持,有时说支持gzip,接收后又解压失败。这时候如果缓存已经存了压缩版本,后续相同设备访问就容易命中错误内容。建议在服务端做兼容性兜底,对已知有问题的User-Agent强制返回未压缩资源,同时避免把这些异常请求污染主流缓存池。
另外,静态资源尽量使用文件指纹命名,比如app.2c91808.js。这类文件一旦生成就不会变,配合长期缓存和gzip压缩,反而能提升命中率——因为内容固定,压缩结果也稳定,缓存可以安心复用。
别忘了反向代理的配置
如果你用nginx做反向代理,记得检查gzip配置是否一致。比如后端应用已经压缩了响应,而nginx又套一层gzip,不仅浪费CPU,还会让缓存失效。典型配置应该是:
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
gzip_vary on;
其中gzip_vary on; 会自动添加Vary头,省去手动设置的麻烦。但要注意,如果上游已经加了Vary,这里再开一次可能导致重复,虽然不影响功能,但不够干净。
最终效果取决于整个链路的协同。从源站到CDN,再到用户设备,每个环节对gzip的处理方式都会影响缓存的归类和复用效率。压缩虽小,牵动全局。