gzip 从 1 级调到 9 级只省 0.7%,brotli q=1 反而更快更小
接口返回 1.5MB JSON,压不压缩差 12 倍。但把 gzip 从 1 级调到 9 级,省下的只有 0.7 个百分点,CPU 却多烧了 4 倍。这个比例值得先算清楚再写配置。
实验
造一份贴近真实响应的数据:10000 条结构相同的记录,JSON.stringify 后 1568447 字节。用 Node 内置 zlib 各压 20 轮,取平均。链路按 20 Mbps 下行折算传输时间。
| 方案 | 压缩后 | 省下 | 压缩耗时 | 传输 | 合计 | 单核吞吐 |
|---|---|---|---|---|---|---|
| 不压缩 | 1568447 B | 0% | — | 627.4 ms | 627.4 ms | — |
| gzip 1 级 | 120216 B | 92.3% | 4.9 ms | 48.1 ms | 53.0 ms | 203 个/秒 |
| gzip 6 级 | 113145 B | 92.8% | 14.2 ms | 45.3 ms | 59.5 ms | 70 个/秒 |
| gzip 9 级 | 109182 B | 93.0% | 22.0 ms | 43.7 ms | 65.7 ms | 45 个/秒 |
| brotli q=1 | 83804 B | 94.7% | 2.8 ms | 33.5 ms | 36.3 ms | 354 个/秒 |
| brotli q=11 | 27055 B | 98.3% | 4870 ms | 10.8 ms | 4880 ms | 0 个/秒 |
三个结论摆在一起很扎眼。
第一,压缩的收益几乎全在「压不压」这一步。 从 0% 到 92.3%,是 12 倍差异;从 92.3% 到 93.0%,只有 0.7 个百分点。因为这份数据高度重复——字段名、时间戳格式、example.com 后缀反复出现,LZ 窗口很快就吃满了。再往后每多一级,算法在做的是更贵的哈希链搜索,换来的边际字节越来越少。
第二,级别 9 是最典型的「看起来很努力」的配置。 22 ms 换 3963 字节,按 20 Mbps 折算只值 1.6 ms 传输时间。也就是说你花了 22 ms 的 CPU 去省 1.6 ms 的网络——在服务端,这笔账永远是亏的。单核吞吐从 203 个/秒掉到 45 个/秒,等于把四台机器才能干完的活压到一台机器上。
第三,gzip 不是最优解。 同样是速度优先档,brotli q=1 压缩耗时只有 gzip 1 级的一半多,压出来的体积还小 30%(120216 → 83804)。原因是 brotli 内置了一份静态字典,装了大量常见的 HTTP/JSON 片段,短文本和结构化数据的起点比 gzip 高。
而 brotli q=11 是另一个极端:4.87 秒,单核一秒压不完一个响应。这个级别只适合构建期预处理静态文件,放在请求链路上等于自杀。
落到配置上
- 动态响应(每次都不同):
brotli q=1,退化到gzip level 1。别用默认级别,Nginx 的gzip_comp_level默认是 1,但很多人会手动改成 6 甚至 9。 - 小于 1KB 的响应别压。 帧头加字典开销可能比省下的字节还多,Nginx 的
gzip_min_length 1024就是干这个的。 - 已经压过的内容别再压:图片、视频、zip、woff2。二次压缩纯浪费时间,还可能变大。
- 静态资源在构建期用 brotli q=11 预生成,运行时直接发
.br文件,CPU 成本为零,这时候才轮得到高质量档。
一个容易忽略的点
压缩耗时和你压多少字节强相关。上面是 1.5MB 的大响应,所以 5 ms 和 22 ms 的差距看得清楚。换成 2KB 的小响应,gzip 1 级和 9 级都在几十微秒,级别随便设都无所谓——真正卡住吞吐的场景,一定是大 JSON 列表接口。
所以别记结论,记方法:拿你最大的一两个接口,把响应体 dump 下来,跑一遍上面这张表,再决定级别。数字比习惯可靠。
- 点赞
- 收藏
- 关注作者
评论(0)