gzip,Brotli,Zstd HTTP 压缩:开发者无意中设置了 content-encoding: identity 的情况
HTTP 压缩协商的工作原理(Accept-Encoding / Content-Encoding),gzip、Brotli 和 Zstd 的并列性能对比,使用 curl 验证压缩是否真正生效,以及四种静默禁用压缩的错误配置。
您的 nginx 配置包含 gzip on;。您的应用程序返回 JSON。响应体仍为 35KB 未压缩。没有错误,没有警告——压缩只是默默地没有发生。
这通常是四种配置错误之一。但首先:协商机制是如何工作的。
HTTP 压缩协商机制
两个头部,除此之外没有其他内容。客户端在 Accept-Encoding中宣布它能解压的内容。 Content-Encoding:
GET /api/data HTTP/1.1
Host: example.com
Accept-Encoding: gzip, deflate, br, zstd
HTTP/1.1 200 OK
Content-Type: application/json
Content-Encoding: br
Vary: Accept-Encoding
这 Vary: Accept-Encoding header 是非可选的,如果您关心 CDN 缓存的正确性。如果没有它,CDN 可能会缓存一个 Brotli 压缩的响应,并将其提供给仅宣布了 gzip 在 Accept-Encoding的客户端。 gzip_vary on; 。该客户端随后尝试将 Brotli 解压为 gzip,结果得到垃圾数据。nginx 会自动添加这个头部。
Content-Encoding: identity 设置为“无编码”在技术上是有效的,但没有人显式设置它。实际的故障模式恰恰相反:当您期望看到它时,根本没有这个 header。 Content-Encoding header 是非可选的,如果您关心 CDN 缓存的正确性。如果没有它,CDN 可能会缓存一个 Brotli 压缩的响应,并将其提供给仅宣布了
验证压缩是否真正生效
在调试配置之前,请确认问题:
# Check headers only
curl -sI -H "Accept-Encoding: gzip, br, zstd" https://example.com/api/data | grep -i "content-encoding\|vary"
# Compare compressed vs uncompressed size
curl -so /dev/null -w "uncompressed: %{size_download} bytes
" https://example.com/api/data
curl -so /dev/null --compressed -w "compressed: %{size_download} bytes
" https://example.com/api/data
--compressed 会自动发送 Accept-Encoding: deflate, gzip, br, zstd 并解压响应。如果两个数值匹配,说明压缩功能未启动。如果您想检查所有响应头部及其在上下文中的含义,可以使用 HTTP 标头分析器 来注释这些头部,包括 Vary, Content-Encoding和 cache-control 指令。
gzip 与 Brotli 与 Zstd
目前对 HTTP 实际上相关的主要算法有三种。以下基准数据来自 Zstd 官方基准测试 在 Silesia 数据集上的结果——这是一个混合现实世界文件(HTML、源代码、PDF、数据库)的标准数据集,在 Core i7-9700K 上测试。纯 JSON 或纯文本负载通常压缩效果更好。
| Algorithm | 等级 | 比值 | 压缩 | 解压缩 |
|---|---|---|---|---|
| gzip | 1(快速) | 2.74x | 69 MB/s | 380 MB/s |
| gzip | 6(默认) | 2.97x | 29.9 MB/s | 360 MB/s |
| gzip | 9(最大) | 3.10x | 18 MB/s | 360 MB/s |
| Brotli | 4 | 3.18x | 104 MB/s | 440 MB/s |
| Brotli | 11(最大) | 3.74x | 0.4 MB/s | 440 MB/s |
| Zstd | 1(快速) | 2.88x | 430 MB/s | 1,380 MB/s |
| Zstd | 3(默认) | 3.01x | 320 MB/s | 1,350 MB/s |
| Zstd | 19(最大) | 3.40x | 17.5 MB/s | 1,380 MB/s |
gzip 是基准。级别 6 是实时处理的最佳选择——从级别 6 到级别 9 每增加 65% 的 CPU 成本,性能提升约 4%,对于动态响应来说不值得。静态文件的预压缩是另一种计算方式。
Brotli 在 CPU 成本相当的情况下,级别 4-6 优于 gzip,且解压速度约为 20% 更快。原因在于 Brotli 拥有一个针对网络内容优化的静态字典——HTML 实体、HTTP 字段名称、JavaScript 关键字。它在相同材料上比通用压缩器获得更好的压缩比。级别 11 仅适用于预压缩的静态资产;在 0.4 MB/s 的压缩速度下,每分钟只能压缩约 25MB。这属于构建步骤,而非请求处理器。
Zstd 是速度故事。默认级别(3)与 gzip 的压缩比相同,但压缩速度是 gzip 的 10 倍,解压速度几乎快 4 倍。主要限制是浏览器支持:Chrome 118+(2023 年 10 月),Firefox 126+(2024 年 5 月),Safari 18+(2024 年末)。目前尚不足以作为唯一算法使用,但如果服务器配置得当,添加 Zstd 仅需几行配置,即可帮助那些声明支持的客户端。Zstd 在级别 19 时接近 Brotli-11 的压缩比,而无需承受灾难性的压缩速度惩罚,使其在高压缩需求的实时处理中更具实用性。
浏览器和客户端支持
| Algorithm | 铬合金 | Firefox | Safari | 边缘 | Node.js |
|---|---|---|---|---|---|
| gzip | 全部 | 全部 | 全部 | 全部 | 内置(zlib) |
| deflate | 全部 | 全部 | 全部 | 全部 | 内置(zlib) |
| Brotli (br) | 51+ | 44+ | 11+ | 15+ | v10.16+ |
| Zstd | 118+ | 126+ | 18+ | 118+ | v21+ |
一个重要的特性: br 且 zstd 仅在 Accept-Encoding HTTPS 连接中出现。浏览器故意不通过普通 HTTP 传输这些头部——这是为了防止中间人攻击注入编码头部。如果您在 http://localhost 上测试并发现只看到 gzip, deflate,这就是原因。请通过 HTTPS 测试,或直接使用 curl(curl 不会应用此限制)。
四种静默破坏其功能的配置错误
1. 缺少 gzip_proxied(nginx 反向代理)
nginx 的 gzip 模块会压缩其自身生成的响应。对于代理请求(上游应用到 nginx 再到客户端),您需要 gzip_proxied ——否则 nginx 只会压缩其自身内容处理器生成的响应,而不会压缩来自 proxy_pass 上游的响应。
# This is NOT enough when nginx is a reverse proxy:
gzip on;
gzip_types text/plain application/json application/javascript text/css;
# You need this too:
gzip_proxied any;
大多数 nginx 配置是反向代理。大多数教程忽略了 gzip_proxied。这两个事实解释了大量未压缩响应的问题。
2. MIME 类型未包含在 gzip_types 中
nginx 的默认 gzip_types 是 text/html 仅包含。JSON、CSS、JavaScript、SVG——除非明确列出,否则均不压缩:
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml
application/rss+xml
image/svg+xml;
nginx 根据基础 MIME 类型进行匹配,因此 application/json 覆盖了 application/json; charset=utf-8。无需单独列出字符集变体。
3. 中间代理剥离了 Accept-Encoding
AWS ALB、配置错误的 Cloudflare Workers 和某些 API 网关设置会在响应到达源之前剥离或重写 Accept-Encoding 。服务器从未看到该头部,因此默认不进行压缩,所有下游组件都认为该功能已损坏,而实际问题出在中间件。整个链路中没有任何错误提示。
通过比较源响应与 CDN 响应来调试:
# Via CDN/proxy
curl -sI -H "Accept-Encoding: gzip, br" https://example.com/api/data
# Direct to origin (bypassing CDN via --resolve or direct IP)
curl -sI -H "Accept-Encoding: gzip, br" --resolve "example.com:443:ORIGIN_IP" https://example.com/api/data
如果源直接返回 Content-Encoding: gzip ,但 CDN 响应中没有 Content-Encoding,则 CDN 正在剥离某些内容——更可能是剥离了 Accept-Encoding 以使源在最初阶段就无法压缩。
4. 上游应用已压缩,nginx 再次尝试压缩
如果您的 Node.js/Go/Python 应用已压缩响应体并设置了 Content-Encoding: gzip,nginx 应跳过双重压缩——但这取决于头部的发送时机。如果上游在流传输中发送头部,或 nginx 的检测发生竞争,您可能会得到双重压缩的垃圾数据,客户端无法解码。
解决方案:让 nginx 全权负责压缩。从您的应用中移除压缩中间件(如 express 的 compression 模块,Go 的 gzip.Handler等),返回原始响应,由 nginx 在边缘进行压缩。获得相同的性能提升,且无双重压缩风险。
有效配置
nginx
gzip on;
gzip_vary on; # adds Vary: Accept-Encoding automatically
gzip_proxied any; # compress responses from proxied upstreams
gzip_comp_level 6;
gzip_min_length 256; # skip tiny responses where overhead isn't worth it
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml
application/rss+xml
image/svg+xml;
# Brotli requires the ngx_brotli module
# https://github.com/google/ngx_brotli
brotli on;
brotli_comp_level 4;
brotli_static on; # serve pre-compressed .br files when they exist
brotli_types
text/plain
text/css
application/json
application/javascript
image/svg+xml;
Apache
LoadModule deflate_module modules/mod_deflate.so
AddOutputFilterByType DEFLATE text/html text/plain text/css application/json application/javascript image/svg+xml
Header append Vary Accept-Encoding
# mod_brotli requires Apache 2.4.26+
LoadModule brotli_module modules/mod_brotli.so
AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/css application/json application/javascript image/svg+xml
Caddy
Caddy 默认启用 gzip 和 Brotli。要显式添加 Zstd:
example.com {
encode gzip zstd br
reverse_proxy localhost:3000
}
无需 MIME 类型列表,无需 gzip_proxied 边缘情况,正确处理 Vary 问题。对于“哪个服务器的压缩相关配置错误表面面积最小”的问题,诚实的答案是 Caddy。
在您自己的负载上测试压缩
Silesia 数据集上的基准数字只能说明相对性能,但您的具体负载更为重要。一个具有稳定字段名称的重复 JSON API 响应与最小化 JavaScript 或混合 HTML 的压缩效果不同。这些工具允许您在浏览器中测试特定负载,而无需启动本地压缩服务器:
- Gzip / Zlib / Deflate 测试器 ——粘贴您的负载,立即查看压缩后的大小和比率
- Brotli 压缩编码器/解码器 ——在不同质量级别下测试 Brotli 压缩
- Zstandard (Zstd) 压缩工具 ——在浏览器中进行 Zstd 编码/解码
在决定是否在 Brotli-11 下预压缩静态资产,还是让 nginx 实时处理 gzip 时非常有用。粘贴您的实际响应负载,比较比率,用真实数据做出决策。
总结
如果响应未被压缩且 curl -sI 确认没有 Content-Encoding,修复几乎肯定是上述四种配置错误之一——最可能是 gzip_proxied any; 的 nginx,或 CDN 吃掉了您的 Accept-Encoding 头部。在责备服务器配置之前,请直接检查源响应。
对于算法选择:动态 API 响应使用 gzip-6 是合适的,且配置风险几乎为零。为静态资产添加 Brotli——在构建步骤中预压缩至级别 11,通过 brotli_static on提供服务,并让 nginx 为不声明 br的客户端回退到 gzip。现在添加 Zstd 是值得的;配置成本微乎其微,且其浏览器支持正在快速增长。为新项目提供 gzip、Brotli 和 Zstd 三种算法,并正确处理 Vary: Accept-Encoding 是正确的做法。
