gzip 已经是每个站长耳熟能详的压缩方案,但浏览器早就在悄悄支持一种更好的算法——Brotli。它由 Google 推出,在文本类内容(HTML/CSS/JS)上通常比 gzip 再小 15%~20%,而对服务器 CPU 的额外开销很小,尤其是配合预压缩或低压缩级别时几乎可以忽略。对内容站来说,省下来的每一 KB 都是真金白银的带宽与更快的首屏。这篇文章讲清 Brotli 的原理、Nginx 上怎么装、怎么配、以及怎么验证它真的生效了。
一、Brotli 为什么比 gzip 更小
两者都是无损压缩,差距来自算法设计:
Brotli 使用内置静态字典,里面预置了大量 HTML、CSS、JavaScript 常见片段(如 <div class=、function、常见英文单词)。网页内容高度重复,字典命中率高,小文件尤其占便宜。
gzip(DEFLATE)没有这种针对 Web 的内置字典。
Brotli 的压缩级别 1~11,级别越高越慢越小。实践中 4~6 级是性价比甜点区:比 gzip 9 级更小,速度却可能更快。
一个直观经验:静态 HTML/CSS/JS 用 Brotli 后体积常再降 15%~20%;对已经很小的文件或随机二进制内容,收益有限甚至不划算——所以要按类型选择性启用。
二、为什么 Nginx 自带没有 Brotli
Nginx 官方发行版只内置 gzip,Brotli 需要第三方模块 ngx_brotli。这意味着两条路:重新编译 Nginx,或用打包好该模块的发行版/镜像。OpenResty 和不少 Linux 发行版(如 Debian 的部分第三方源、Alpine 的部分包)已经带了。选择路径前先查清楚你当前 Nginx 是怎么装的。
检查当前是否已支持:
nginx -V 2>&1 | tr ' ' 'n' | grep -i brotli
无输出 = 未编译进该模块
三、编译安装 ngx_brotli
先装编译依赖(Debian/Ubuntu 例):
apt-get install -y build-essential libpcre3 libpcre3-dev zlib1g-dev libssl-dev
拉取模块(含子模块):
git clone --recurse-submodules https://github.com/google/ngx_brotli.git /usr/local/src/ngx_brotli
然后进入你的 Nginx 源码目录,用与你原来完全相同的 configure 参数,在末尾追加 --add-dynamic-module:
./configure $(nginx -V 2>&1 | sed -n 's/^configure arguments: //p' | sed 's/--add-dynamic-module=1*//g') \
--add-dynamic-module=/usr/local/src/ngx_brotli
make modules
编译完成后把产物拷进模块目录:
mkdir -p /etc/nginx/modules
cp objs/ngx_http_brotli_filter_module.so /etc/nginx/modules/
cp objs/ngx_http_brotli_static_module.so /etc/nginx/modules/
两个坑务必注意:
一定要用原来的 configure 参数重新编译,否则新 Nginx 二进制会丢掉你原有的模块(WAF、GeoIP、自定义补丁等)。用 nginx -V 抄参数是标准做法。
动态模块的 .so 必须与主程序 ABI 匹配,别把 A 机器编的 so 拷到 B 机器用,路径和版本都要对上。
四、加载模块并配置
在 /etc/nginx/nginx.conf 顶部(events 块之前)加载:
load_module modules/ngx_http_brotli_filter_module.so;
load_module modules/ngx_http_brotli_static_module.so;
然后在 http 块里开启:
brotli on;
brotli_comp_level 5;
brotli_static on;
brotli_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/json
application/xml
application/rss+xml
image/svg+xml;
参数含义:
brotli on:开启动态压缩。
brotli_comp_level:1~11,默认 6。级别 5~6 一般够用;对静态资源追求极致可上 9~11,但务必配合缓存,别让每个请求都现压。
brotli_static on:如果有 .br 预压缩文件(如 app.js.br),优先直接发给支持 Brotli 的客户端,零 CPU 开销。这是内容站的最优解。
brotli_types:只压缩文本类,别把图片/视频塞进来(它们本身已压缩,再压只会浪费 CPU)。
改完 nginx -t 检查,再 systemctl reload nginx。
五、同时保留 gzip,让 Nginx 智能协商
老旧客户端可能不支持 Brotli,所以不要关掉 gzip,让两者并存:
gzip on;
gzip_comp_level 5;
gzip_min_length 256;
gzip_types text/plain text/css application/javascript application/json application/xml image/svg+xml;
gzip_vary on;
Nginx 会优先给支持 br 的客户端发 Brotli,否则退回 gzip。用 gzip_vary on(或 Brotli 对应的 Vary 头)确保中间缓存/CDN 不会把压缩内容发给不支持的客户端。这一步不能省——曾经有站点因为缺 Vary: Accept-Encoding,导致 CDN 缓存了一份 Brotli 响应发给旧浏览器,页面直接乱码。
六、验证它真的生效
第一步:请求头协商。
curl -sI -H "Accept-Encoding: br" https://你的域名/ -o /dev/null -D - | grep -i "content-encoding|content-length|vary"
期望看到 Content-Encoding: br。如果返回的是 gzip 或空白,说明没生效。
第二步:对比体积。同一 URL,分别请求 br 与 gzip,比 content-length:
echo "br:"; curl -sI -H "Accept-Encoding: br, gzip" https://你的域名/style.css | grep -i content-length
echo "gzip:"; curl -sI -H "Accept-Encoding: gzip" https://你的域名/style.css | grep -i content-length
br 应明显更小。如果两者一样,可能是:文件太小被 gzip_min_length/brotli_min_length 跳过,或类型不在 brotli_types 里,或该响应走了预压缩的 gzip 缓存。
第三步:CDN/回源链路。如果你前面挂了 CDN,注意 CDN 可能自己处理压缩,甚至会把 Content-Encoding 改写。验证时要看「浏览器实际收到的那一份」,而不是只测源站。必要时临时绕过 CDN 直连源站 IP 对照。
第四步:确认没有被二次压缩。同时开动态 Brotli 和静态 .br 时,若服务器还想对已经是 br 的内容再压一次,会出错。确保 brotli_static 与动态压缩的分工清晰,且 Nginx 不会对已带 Content-Encoding 的响应重复处理。
七、性能与成本权衡
静态资源用预压缩:构建时生成 .gz 与 .br,线上直接发,CPU 零成本,效果最强。这是最推荐的方案。
动态页面用低级别:HTML 每次不同,只能动态压。级别 4~5 兼顾体积与速度,别在动态页面上开到 11。
观察 CPU:上线后用 top/pidstat 看 nginx worker 的 CPU 变化。若明显上升且站点是 CPU 瓶颈,把级别调低或改为只对静态资源开 Brotli。
别把已压缩格式加进来:jpg/png/webp/woff2 再压基本无收益,纯粹烧 CPU。
八、常见翻车与回退
如果上线后出现页面乱码、下载文件损坏、或 CDN 报错,按顺序回退:
brotli off; 并 reload,确认问题是否消失。
检查是否漏了 Vary: Accept-Encoding,导致缓存串味。
检查是否对二进制类型误开了压缩(brotli_types 写宽了)。
检查是否为动态模块 ABI 不匹配导致 worker 崩溃(journalctl -u nginx 看 core dump)。
彻底回退:注释掉 load_module 两行并 reload,回到纯 gzip,业务零影响。
Brotli 的收益是「确定且可量化」的——相同内容更小的字节数、更少的带宽花费、某个程度上也更快的首屏。而它的风险是「可控且可回退」的。对内容站来说,这是性价比很高的一次优化。把它正确装上、验证到位、留好回退开关,你就能安心享受那额外省下的 15%~20% 流量。
九、预压缩:把 CPU 成本挪到构建期
对静态资源(CSS/JS/字体/HTML),最优解不是让 Nginx 每次请求现压,而是在部署时就把压缩结果生成好,线上直接发。以 app.css 为例:
生成 .br(Brotli,质量 11 追求最小)
brotli -q 11 -k -o app.css.br app.css
生成 .gz(gzip 兜底,级别 9)
gzip -9 -k app.css
然后用 brotli_static on;,Nginx 遇到支持 br 的客户端会优先找 app.css.br 直接发送,完全不消耗运行时 CPU。配合 gzip_static on; 做 gzip 兜底,一套完整的「零运行时开销」压缩链路就成型了。
几个注意点:
预压缩文件的 .br/.gz 必须与源文件同目录同名,Nginx 才会识别。
更新源文件后要同步重新生成压缩版本,否则线上发的还是旧的。建议把这两步写进构建脚本,别手动记。
若源文件更新但 mtime 变了、压缩文件没更新,Nginx 可能仍发旧的 .br,表现为「改了代码页面却没变」。构建脚本里先删旧 .br 再生成。
十、CDN 与 Brotli 的配合
内容站大多挂了 CDN,这会让 Brotli 的验证复杂一层。关键认知:浏览器最终收到的那一份,才是要验证的。三种常见形态:
源站压,CDN 透传。 你开 Brotli,CDN 尊重 Content-Encoding 原样转发。此时回源和边缘都要保持 Vary: Accept-Encoding,否则缓存串味。
源站不压,CDN 压。 一些 CDN 自带 Brotli 压缩开关,边缘节点按客户端的 Accept-Encoding 实时压缩。此时你源站开不开 Brotli 意义不大,重点在 CDN 控制台。
两边都压。 最容易出问题——源站发了 br,CDN 又压一次,双重编码导致乱码。要么源站关、要么 CDN 关,别叠加。
验证时用 curl -I --compressed 看边缘返回的 Content-Encoding,必要时直连源站 IP 用 -H "Host: 你的域名" 对照,才能分清是谁在压。
十一、省流收益到底有多少
给一个可自测的方法,让你知道值不值得上:
取一个真实静态文件,比较三种方式的大小
SIZE_RAW=$(curl -s https://你的域名/style.css | wc -c)
SIZE_GZ=$(curl -s -H "Accept-Encoding: gzip" https://你的域名/style.css | wc -c)
SIZE_BR=$(curl -s -H "Accept-Encoding: br" https://你的域名/style.css | wc -c)
echo "raw=$SIZE_RAW gzip=$SIZE_GZ br=$SIZE_BR"
经验区间:文本类(HTML/CSS/JS)Brotli 通常比 gzip 再小 15%~20%;JSON 接口响应收益也明显;而图片、视频、字体(woff2 本身已压)收益接近零。所以把 brotli_types 限制在文本类,既拿到主要收益,又不浪费 CPU。
对一个月跑几百 GB 流量的内容站,省 15% 就是实打实的带宽账单下降;对流量小的站,省下的更多是「首屏快那么一点点」的体验提升。收益虽随规模变化,但方向是一致的:更小的字节,永远不亏。
(adsbygoogle = window.adsbygoogle || []).push({});