Nginx gzip 与 Brotli 压缩优化实战:压缩类型取舍、静态预压缩、HTTP/2 协同与「配了不生效」排查

压缩为什么是性价比最高的提速手段

聊网站性能,很多人第一反应是升级服务器、上 CDN、换框架。但这些要么花钱,要么动大手术。而开启文本压缩,往往只是改几行配置,就能把 HTML、CSS、JS、JSON 这类文本响应的体积砍掉 60% 到 80%。对带宽有限的个人小站来说,这意味着首屏资源传输更快、月流量更省、用户体验直接见效,可以说是投入产出比最高的一项优化。

这篇讲清楚三件事:gzip 怎么配才对、brotli 比 gzip 强在哪、以及 HTTP/2 之下这些压缩如何协同。全程以 Nginx 为主线,给出可复制的配置和几个常见的坑。

先搞清楚:到底哪些类型值得压缩

压缩是有代价的——服务器要花 CPU 去压缩,客户端要花时间去解压。所以不能无脑全压。基本判断标准是:文本类、可压缩比高的类型开启;已经压缩过的二进制格式(图片、视频、字体包)绝对不能重复压,因为它们本身就压过了,再压既没收益又浪费 CPU。

典型的处理策略是:压缩 txt、html、htm、css、js、json、xml、svg 这些文本资源;对 png、jpg、gif、webp、avif、mp4、zip、gz、woff2 直接跳过。woff 字体值得一提:老式 woff 可以压,但 woff2 内部已经是压缩格式,压了没意义。

Nginx gzip 的正确配置

Nginx 自带 gzip 模块,配置写在 http 块里(或 server 块内,但 http 全局更省事)。一套经过实战的配置如下:

gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_disable "msie6";
gzip_types
    text/plain
    text/css
    text/xml
    text/javascript
    application/javascript
    application/json
    application/xml
    application/rss+xml
    image/svg+xml;

逐条说明。gzip_comp_level 5 是压缩级别,范围 1 到 9。级别越高压得越小但 CPU 越贵,实测 5 到 6 是收益曲线的拐点——再往上(比如 9)体积只多压 1% 到 2%,CPU 却翻倍。对 CPU 有限的小 VPS,5 是最稳妥的选择。

gzip_min_length 1024 表示小于 1KB 的响应不压。太小的文件压完可能还更大(要加上 gzip 头部),压了反而亏。gzip_vary on 会在响应里加上 Vary: Accept-Encoding,这是让 CDN 和缓存正确区分「压缩版」和「未压缩版」的关键,漏了它 CDN 可能把压缩内容发给不支持 gzip 的客户端,导致乱码。gzip_proxied any 表示即使请求经过代理(比如你前面有 CDN 或反代)也照样压。

还有一个新手常犯的错:gzip_types 里默认不包含 text/html,因为 Nginx 对 html 默认就压,不用显式列。text/html 你加了也无妨,但要注意 gzip_types 是「追加」语义,不是覆盖。另外,MIME 类型必须和实际响应头完全一致,比如很多站返回 JS 用的是 text/javascript,而你只写了 application/javascript,那就压不到——这是「配了却没生效」最隐蔽的原因。

验证 gzip 是否真的生效

改完记得 nginx -t 校验再 systemctl reload nginx。然后用 curl 带上 Accept-Encoding 头来验证:

curl -s -H 'Accept-Encoding: gzip' -I https://example.com/style.css | grep -i content-encoding

如果返回里有 Content-Encoding: gzip,说明生效了。想对比体积,可以分别请求压缩和不压缩两个版本,看 Content-Length 的差异,或者用 curl -s -o /dev/null -w '%{size_download}' 直接看下载到了多少字节。注意如果前面有 CDN,curl 打到的是 CDN 节点,验证时要确认 CDN 是否透传或自己做了压缩——有时 Nginx 配了,但你请求到的其实是 CDN 缓存的老版本,加个随机 query 参数绕过缓存再看。

Brotli:比 gzip 更小,值得上的下一代压缩

Brotli 是 Google 推出的压缩算法,现代浏览器(Chrome、Firefox、Edge、Safari)全都支持,而且这是通过 Accept-Encoding: br 协商的,不支持的旧客户端会自动回落到 gzip,非常安全。它的压缩率通常比 gzip 高 15% 到 25%,尤其对 HTML 和 JS 这类结构化文本优势明显。代价是压缩时 CPU 略高,但静态资源可以「预压缩」一次、长期复用,这个代价可以忽略。

Nginx 官方不内置 brotli,需要装 ngx_brotli 模块。如果你用的是发行版自带的 Nginx,通常没有这个模块,需要换成带模块的包或自己编译。以常见的做法为例,编译参数加上:

--add-module=/path/to/ngx_brotli

配置和 gzip 很像:

brotli on;
brotli_comp_level 5;
brotli_min_length 1024;
brotli_types
    text/plain
    text/css
    text/javascript
    application/javascript
    application/json
    application/xml
    image/svg+xml;

建议 gzip 和 brotli 同时开启,让 Nginx 根据客户端的 Accept-Encoding 自动选择。brotli 级别同样建议 4 到 6,级别 11 虽然压得最小但 CPU 极贵,只适合离线预压缩静态文件,不适合在线动态压缩。

静态资源预压缩:把 CPU 开销降到零

对不会随时变化的静态文件(CSS、JS、已经构建好的 HTML),可以在部署时用 brotli 命令和 gzip 命令生成 .br 和 .gz 副本,Nginx 用 gzip_static 和 brotli_static 直接发预压缩文件,不再实时压缩:

gzip_static on;
brotli_static on;

预压缩命令示例:

find /var/www/example -type f \( -name '*.css' -o -name '*.js' -o -name '*.html' \) \
  -exec sh -c 'gzip -9 -k -f "$1"; brotli -q 11 -k -f "$1"' _ {} \;

这样每个静态文件旁边都有 .css.gz 和 .css.br,Nginx 优先发 .br(客户端支持的话),其次 .gz,都不可用时才实时压缩。静态站这样做,压缩成本几乎为零,收益却拉满。记得把 .gz .br 加入构建流程或部署脚本,每次更新资源后重新生成。

HTTP/2 与压缩的协同

HTTP/2 开启了多路复用,一个连接上可以并发传输很多资源,头信息还能用 HPACK 压缩。它和 gzip/brotli 是互补关系:brotli 压的是「响应体」,HPACK 压的是「头部」。两个都开,效果叠加。

但 HTTP/2 有一个反直觉的注意点:它让「合并文件」和「域名分片」这类 HTTP/1.1 时代的优化手段失去了意义,甚至有害。所以在 HTTP/2 下,你应该把 CSS/JS 拆得更细粒度(方便按需加载和缓存),而不是硬合成一个大包。压缩依然重要,因为每个细粒度文件都独立传输,压得越小总传输量越小。开启 HTTP/2 只需在 listen 后面加 http2(Nginx 1.25.1 之后应改用独立的 http2 on; 指令):

listen 443 ssl;
http2 on;

几个「配了不生效」的实战坑

第一,CDN 覆盖了压缩设置。你 Nginx 开了 brotli,但 Cloudflare 等 CDN 回源后自己重新处理,最终发给浏览器的是 CDN 决定的编码。要统一策略,要么在 CDN 后台也开对应压缩,要么让 CDN 透传源站的 Content-Encoding。排查方法:直接 curl 源站 IP(绕过 CDN)和对域名分别测,看结果是否一致。

第二,MIME 类型不匹配。前面说过,服务器实际返回的 Content-Type 必须和 gzip_types/brotli_types 里列的一致,否则不压。用 curl -I 看真实的 Content-Type,再对照配置,别凭记忆。

第三,压缩和缓存打架。如果 CDN 缓存了一份「未压缩」的响应,又发给了支持压缩的客户端,就浪费了压缩红利;反过来把压缩内容发给了不支持解压的客户端会乱码。这正是 Vary: Accept-Encoding 要解决的问题,CDN 必须尊重这个响应头,否则缓存键里要包含 Accept-Encoding。

第四,压得太狠伤 CPU。见过有人把 gzip_comp_level 设成 9,结果小机器 CPU 跑满、请求排队。记住在线压缩用 5 到 6,要极致压缩就用预压缩。top 或 htop 里看 nginx worker 的 CPU 占用,是最直接的判断依据。

压缩之前的「去冗余」:比压缩更狠的一步

压缩是在「已经交付的资源」上做减法,但更有效的是从源头把资源变小。CSS 和 JS 文件里大量的缩进、换行、注释、多余空格,以及 CSS 里没用的规则,都是可观的体积来源。在开启 gzip/brotli 之前,先做一遍 minify,收益往往是叠加的——先删掉 30% 的冗余,再压掉剩下的 70%,最终体积能比只压缩小一大截。

前端构建工具链里这步通常是自动的。如果用 Node 生态,构建产物默认就是压缩过的;但很多个人站直接手写 CSS/JS 挂上去,没走构建流程,那就要手动或半自动合并压缩。一个简单的办法是用 cssnano 和 terser 命令行处理:

npx cssnano input.css output.min.css
npx terser input.js -c -m -o output.min.js

-c 是启用压缩,-m 是混淆变量名(同时减小体积)。处理完记得同步更新 HTML 里的引用路径,并用文件哈希做缓存指纹,避免浏览器拿到旧版本。这里要提醒一句:不要对已经压缩过的文件再手动格式化后重新压,容易把语义改坏,尤其是 JS 里的 // 注释和正则表达式,工具处理顺序搞错会出 bug。

小结

压缩优化没有花哨的技巧,核心就三点:压对的类型(文本压、二进制不压)、用对的级别(在线 5 到 6,静态预压缩到 9 到 11)、配好协同(Vary 头、CDN 策略、HTTP/2)。一个个人站,花半个小时把 gzip 配规范、给静态资源加上 brotli 预压缩,往往比折腾半天其他高级优化带来的体感提升还明显。先把这块地基打牢,再谈别的。

Last modification:October 12th, 2026 at 10:25 pm

Leave a Comment