每一次都把同一个文件压一遍
Apache 是即时压缩响应的。对一个每次都不一样的页面来说,这是唯一的办法。对一个一个月才改一次的样式表来说,这意味着服务器为每一位访客把同样的活儿重做一遍,一做几个星期,而且每次都把结果扔掉。这件事没有缓存。
2026 年 8 月 19 日在一条已建立的连接上实测,用的是本站自己的 style.css(124 kB)和 world.js(102 kB):
| 时间 | 字节 | |
| 不压缩 | 2,9 毫秒 | 124 755 |
| gzip,即时 | 18,1 毫秒 | 34 995 |
| brotli,即时 | 14,5 毫秒 | 32 685 |
| world.js,gzip | 47,9 毫秒 | 38 283 |
| world.js,brotli | 15,9 毫秒 | 38 086 |
每个请求四十八毫秒的 CPU,只为得到一个逐字节完全相同的结果。在树莓派上,这不是舍入误差。
压一次,压到磁盘上
一个构建步骤把压好的文件写在原文件旁边,一条重写规则在浏览器声明接受 gzip 时把它送出去。送一个现成的文件,花的就是送任何文件的成本。
用 gzip 而不用 brotli,理由很无趣:这台机器上没装 brotli 命令。文件因此比 brotli 压出来的大约 7 %,而服务器时间只有即时 brotli 的五分之一。这笔交换值得现在就做,而不是等一个更漂亮的答案。
版本号写在文件名里
构建写出的是 style.css.<mtime>.gz,而页面里的 ?v= 就是同一个修改时间。只有当带着对应数字的文件存在时,重写规则才会触发。
这个安排有一个值得刻意去设计的性质:如果你改了样式表却忘了重新构建,页面里的数字指向一个不存在的文件,规则不触发,Apache 就照常把原文件送出去。最坏的情况是加速没了。一份过期的样式表出不去。任何缓存方案都该拿这个问题检验一遍——出问题的时候,它是变慢,还是变错?
中间那个缺陷
被送出的文件本身已经是 gzip,所以出门时不能再压一遍。配置里最初只写了 no-gzip,而它只拦住了 Apache 两个压缩器中的一个。另一个,mod_brotli,兴高采烈地把压好的 gzip 文件又用 brotli 压了一遍,而响应头还一直声称是 gzip。
浏览器拿到的是读不出来的内容。发现它靠的不是看页面——靠的是把字节加起来,发现有个数字说不通。两个都需要,no-gzip 和 no-brotli,而只有其中一个是显而易见的那个。