缓存与压缩策略分析

看这份响应该不该被缓存、能存多久、过期后能不能便宜地确认「没变」,以及压缩到底省了多少字节。

需服务端处理这个工具由服务器代为访问网络

你要检查的网址会发送到本站服务器,由服务器发起查询,结果只返回给这个页面。

服务器不保存查询内容,也不记录你查了什么,只按来源 IP 限制频率(每分钟 30 次,且这个工具一次检查最多发三个请求(压缩版、不压缩版、条件请求))防止被当跳板刷。站上其它工具仍然全部在浏览器本机完成。

例子:

想知道确切的压缩比,就把网址换成具体的 CSS 或 JS 文件:

整个响应超过 1 MB 时不再计算压缩比——两边都会被截断成同样长度,算出来的数字没有意义。

缓存策略的四个问题

配缓存策略其实是在回答四个独立的问题,混在一起想就会乱。第一,能不能存:由 no-store 和 private 决定,前者谁都别存,后者只允许用户自己的浏览器存。第二,能存多久:由 max-age 决定(面向 CDN 的数字是 s-maxage)。第三,过期了怎么办:靠 ETag 或 Last-Modified 发一次条件请求,如果内容没变,服务端回一个 304、不带响应体。第四,中间这一层缓存听谁的:由 public、private、s-maxage 和 must-revalidate 一起决定。

这四条里最容易被忽略的是第三条。声明了 ETag 不代表它会被正确使用,而这正是这个工具专门实测一次条件请求的原因:如果带了 If-None-Match 之后服务端仍然返回 200 和完整内容,说明前面的声明都是白写的——缓存过期后用户还是要重新下载整份资源。常见的成因有两个:ETag 每次请求都重新生成(比如由进程启动时间拼出来),或者中间有一层代理把请求头吃掉了。

第二个常见误区是把 max-age=0 和 no-store 当成一回事。max-age=0 是「每次都要回源确认」,也就是仍然允许存、允许用 304 省下传输;no-store 是「根本不存」。对动态页面来说 max-age=0 加 ETag 是很好的组合,代价只有一次往返的头部交换;而 no-store 会让每次访问都完整回源,除非有隐私上的硬要求,否则没必要。同理,no-cache 的名字极具误导性,它是「存但每次确认」,不是「不缓存」。

还有一点和实际运维关系很大:缓存出问题时的症状往往是「我改了配置或内容,线上没变」。这时先看 Age——它表示这份响应在中间缓存里已经待了多少秒。Age 是个较小值,说明你命中了 CDN;Age 接近 max-age,说明这份副本快过期了。搞清楚自己拿到的是哪一级的副本,比反复刷新页面有效得多。CDN 自己的命中状态通常在 cf-cache-status、x-cache 这类厂商自定义的头里。

压缩:什么时候不该开

文本内容(HTML、CSS、JS、JSON、SVG)开压缩几乎没有争议,Brotli 通常能省 70% 到 85%,gzip 能省 60% 到 80%,而这个开销只在首次压缩时发生。但反过来,已经是压缩格式的内容(JPEG、PNG、WebP、视频、字体、zip)再压一次几乎不会变小,只是白白消耗 CPU——这也是这个工具要实测两次的原因:不实测的话,你看不出服务端到底是在有效压缩还是在空转。

和压缩紧密相关的一个坑是 Vary。响应的内容随 Accept-Encoding 变化时,必须声明 Vary: Accept-Encoding,否则共享缓存可能把 Brotli 版本发给一个只支持 gzip 的老客户端,或者把未压缩版发给支持的客户端。这个工具在检测到「有压缩但 Vary 里没写 accept-encoding」时会明确指出——这类问题在测试环境几乎不会出现,只会在真实用户的浏览器上表现出来,很难现场复现。

最后是压缩比与体积的取舍:压缩只在传输层省带宽,不减少浏览器的解析开销。所以静态资源真正该做的是「带内容指纹的文件名 + 超长 max-age + immutable」,让浏览器一年都不必回源,同时用一个短的 max-age 管住 HTML 本身。这套组合下,ETag 的实际作用只剩「用户强制刷新时省一次重传」。

常见问题

这个工具和「HTTP 响应头在线检查」有什么区别?

那个列的是全部响应头,重点在安全相关的那几项(HSTS、CSP、X-Frame-Options)。这个只看缓存和传输相关的字段,但会多做两件实测:一是带 If-None-Match 再请求一次,看服务端认不认自己给出的验证器;二是分别用压缩和不压缩各取一次,算出真实省了多少字节。响应头里写着 ETag 不等于它被正确使用,这一点只能实测。

相关:HTTP 响应头在线检查

no-cache 是不缓存的意思吗?

不是,这是最容易被名字误导的一条指令。no-cache 的真实含义是「可以存进缓存,但每次使用之前都必须回源确认一次」。真正表示「别缓存」的是 no-store,它连写磁盘都不允许。所以给登录后的页面配 no-cache 是没问题的,配 no-store 更严格一些;而如果是配合 ETag 用的 no-cache,回源确认的代价通常只有几个响应头。

为什么我的静态资源明明配了缓存,还是每次都重新下载?

最常见的原因有两个。一是 Cache-Control 里写了 max-age=0 或没写,浏览器每次都要回源确认——如果这时服务端不支持条件请求(带 ETag 却仍返回 200),那就是整份重新下载,等于缓存白配了。二是 ETag 每次都变,比如它是由进程启动时间拼出来的,那么每次请求的 ETag 都不同,浏览器自然每次都认为内容变了。这两种情况下这个工具的「条件请求实测」都会报出来。

压缩比多少算正常?

文本类内容(HTML、CSS、JS、JSON)用 Brotli 通常能省 70% 到 85%,用 gzip 大约 60% 到 80%。如果测出来只有百分之十几,多半是这份内容本身已经是压缩格式(图片、字体、zip),服务端压了也白压,反而多耗 CPU。反过来,文本内容完全不压缩意味着传输量是几倍,属于明显的浪费。

Age 这个响应头说明什么?

它表示这份响应在中间缓存里已经放了多久(秒)。有 Age 且大于 0,说明你拿到的不是源站现算的,而是某一级缓存里的副本。这个数字要配合 max-age 看:Age 接近 max-age 就说明这份副本快要过期了,接下来的一次访问会触发回源。排查「我改了配置怎么不生效」时,Age 往往能直接给出答案。

为什么检查会请求好几次?

为了让结论是实测而不是猜测。一次拿压缩后的响应,一次明确要求不压缩用来算压缩比,再一次带上验证器测条件请求,最多三次。这也是为什么这个工具的限制是按次数算的。