bigywater.tech
数据编码与压缩 · 18

gzip 与 brotli:HTTP 上的压缩,brotli 的内置字典

浏览器和 HTTP 客户端用 gzip、brotli、zstd 压缩响应。brotli 为什么能在小页面上更小?requests 默认会发什么?对已经压缩的内容再压会怎样?现压缩和预压缩怎么选?这篇在本机服务器上逐项测。

243 对 333
573 字节英文 HTML 样板:brotli q5 与 gzip-9 的压缩后字节(合成乱码词上两者都是 344)
129879 对 150532
2000 条 JSON(723399 字节)线上传输字节:brotli 与 gzip
10993 微秒
brotli q11 现压缩一页 13 KiB HTML 的耗时(gzip level 6 是 94 微秒)

brotli 的字典

实测 · 本机执行输出
  输入                                     原字节  gzip-9  zstd-19 brotli q11 brotli q5
  英文 HTML 样板(导航、页脚)                      573     333      348        203       243
  英文 HTML 样板前 300 字节                     300     214      215        122       153
  合成乱码词(我的 VOC 词表)                       573     344      351        268       344
  products JSON 3 条                     1054     506      500        437       481
  一页 13 KiB 合成页面                       13723    3863     3791       3048      3764

brotli 自带一份静态字典,里面是英文词和 HTML、JavaScript、CSS 的常见片段。实验里 q5 对合成乱码词和 gzip 一样大(344 字节),对英文 HTML 样板却小 27%;这个差别应该来自字典,但我没有读字典文件,只是推断。

常见响应大小上的差别

实测 · 本机执行输出
  输入                                 原字节    gzip-6    gzip-9    zstd-3   zstd-19     br q5     br q9    br q11
  一页 HTML 13 KiB                   13723      3878      3863      3898      3791      3764      3774      3048
  200 条 products JSON Lines        72453     16349     16125     16318     14773     15444     15224     12742
  pages 8 MiB                    8388608   1836858   1822313   1670198   1401606   1588548   1536350   1390770

一页 13 KiB 的 HTML,brotli q11 比 gzip-9 小 21%;200 条 JSON 小 21%。几十 KB 的小响应是 HTTP 的典型大小,所以收益明显。

协商

实测 · 本机执行输出
  requests 2.34.2 默认发出的 Accept-Encoding:'gzip, deflate, br, zstd';服务器选了 br,线上正文 129879 字节,解码后 723399 字节
  Accept-Encoding: identity         请求 /data  → Content-Encoding=None,线上  723399 字节,解码后 723399 字节,解码正确
  Accept-Encoding: identity         请求 /page  → Content-Encoding=None,线上   13723 字节,解码后 13723 字节,解码正确
  Accept-Encoding: gzip             请求 /data  → Content-Encoding=gzip,线上  150532 字节,解码后 723399 字节,解码正确
  Accept-Encoding: gzip             请求 /page  → Content-Encoding=gzip,线上    3890 字节,解码后 13723 字节,解码正确
  Accept-Encoding: br               请求 /data  → Content-Encoding=br,线上  129879 字节,解码后 723399 字节,解码正确
  Accept-Encoding: br               请求 /page  → Content-Encoding=br,线上    3764 字节,解码后 13723 字节,解码正确
  Accept-Encoding: zstd             请求 /data  → Content-Encoding=zstd,线上  138216 字节,解码后 723399 字节,解码正确
  Accept-Encoding: zstd             请求 /page  → Content-Encoding=zstd,线上    3898 字节,解码后 13723 字节,解码正确
  Accept-Encoding: gzip, br, zstd   请求 /data  → Content-Encoding=br,线上  129879 字节,解码后 723399 字节,解码正确
  Accept-Encoding: gzip, br, zstd   请求 /page  → Content-Encoding=br,线上    3764 字节,解码后 13723 字节,解码正确

客户端用 Accept-Encoding 声明支持的编码,服务器选一个。requests 2.34.2 在装了 brotli 和 zstandard 之后默认发 gzip, deflate, br, zstd;2000 条 JSON 线上 gzip 150532 字节、zstd 138216、br 129879。这也是 Vary: Accept-Encoding 的由来。

别压已经压缩的

实测 · 本机执行输出
  内容                              原字节   gzip-6    br q5   zstd-3
  随机字节(像 JPEG/加密数据)           1048576  1048902  1048581  1048609
  已经 gzip 过的文本                 230952   231033   230957   230967
  原始文本(未压缩)                   1048576   230940   200268   209800

随机字节(像 JPEG、加密数据)和已经压缩过的文本再压一次不会变小,还略大。HTTP 压缩应当只用于文本类内容。

现压缩的 CPU

实测 · 本机执行输出
  gzip level 1     4072 字节  中位        44 微秒/页  → 单核每秒约    22727 页
  gzip level 6     3878 字节  中位        94 微秒/页  → 单核每秒约    10667 页
  gzip level 9     3863 字节  中位       134 微秒/页  → 单核每秒约     7474 页
  zstd level 3     3898 字节  中位        26 微秒/页  → 单核每秒约    37915 页
  zstd level 19    3791 字节  中位      2292 微秒/页  → 单核每秒约      436 页
  brotli q4        3874 字节  中位        93 微秒/页  → 单核每秒约    10801 页
  brotli q5        3764 字节  中位       150 微秒/页  → 单核每秒约     6670 页
  brotli q9        3774 字节  中位       218 微秒/页  → 单核每秒约     4585 页
  brotli q11       3048 字节  中位     10993 微秒/页  → 单核每秒约       91 页

q4–q5 和 gzip 一个量级,zstd-3 更快(26 微秒/页),q11 每页 11 ms。动态内容用 gzip-6、zstd-3 或 brotli q4–q5;静态文件预先用 q11 压好。

什么时候压缩划算

实测 · 本机执行输出
  编码              字节        压缩        解压     @1 Mbit/s    @10 Mbit/s   @100 Mbit/s  @1000 Mbit/s
  identity     13723       0微秒       0微秒     109.78 ms      10.98 ms       1.10 ms       0.11 ms
  gzip-6        3878     107微秒      11微秒      31.14 ms       3.22 ms       0.43 ms       0.15 ms
  zstd-3        3898      27微秒      10微秒      31.22 ms       3.15 ms       0.35 ms       0.07 ms
  br q5         3764     147微秒      20微秒      30.28 ms       3.18 ms       0.47 ms       0.20 ms
  br q11        3048   10969微秒      31微秒      35.38 ms      13.44 ms      11.24 ms      11.02 ms
  (总耗时 = 压缩时间 + 字节数×8/带宽 + 解压时间,忽略了延迟、TCP 慢启动、并发;如果压缩是预先做好的静态文件,压缩时间不计入请求)

按“压缩 + 传输 + 解压”的估算:1 Mbit/s 下一页 109.78 ms 降到约 31 ms;1 Gbit/s 下 zstd-3 只有 0.07 ms,而 q11 现压缩是 11.02 ms,不划算。网络越快,压缩的收益越小,压缩的 CPU 开销越要留意。

分步演示

HTTP 压缩
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8(系统 Python,自带 compression.zstd)/ zstandard 0.25.0 / lz4 4.4.5 / zstd、lz4、brotli、protoc 命令行(Homebrew);msgpack、protobuf、snappy 等第三方库临时装在临时目录里,用完删除;所有数据都是本机脚本生成的合成数据(固定随机种子),没有使用任何真实网站或用户的数据;计时用 time.perf_counter,取多次中位数,数字每次运行会有小幅抖动。brotli 1.2.0(Python 包,临时安装)、zstandard、zlib;本机 HTTP 服务器按 Accept-Encoding 选择编码,requests 2.34.2、urllib3 2.7.0;“合成乱码词”是随机音节拼成的,“英文 HTML 样板”是我写的一段典型导航页脚;第五节每种 30 或 300 次取中位数;第六节是用测得的体积和耗时算的估算,不是实测网络
    • 没有验证:brotli 字典文件的内容与大小(只从对比推断它的作用);真实网站的页面与 JS/CSS 文件;浏览器实际的解压行为;HTTP/2 头部压缩(HPACK);zstd 作为 Content-Encoding 在各浏览器里的支持情况;TCP 慢启动、多个请求并发对总耗时的影响
    京ICP备2026067502号-1京公网安备11010502063487号