nginx出厂只压HTML这一种类型,其余的字节全额计入了SEO抓取预算
本文目录
- 爬虫每次拿走的字节里,有多少本来不用传?
- 基准:一次抓取的真实字节分布
- 那15个没压的,都是什么
- nginx出厂到底压了什么?
- 把这四个默认值的代价量出来
- 那一列纹丝不动的JPEG
- 一份可以直接抄的类型清单
- Accept-Encoding里那些没人测过的写法
- q值:写了也白写
- *:规范说匹配一切,nginx当没看见
- identity;q=0:一个大家都不实现的角落
- deflate:浏览器发了二十年的一个空头
- Brotli一定比gzip小吗?
- 现网复核:Brotli赢在93%的URL上
- 压缩级别的边际收益,在哪一档断掉?
- 为什么这一秒对SEO格外贵
- 结论:级别的正确用法是分场景,不是分级别
- 预压缩为什么几乎总是更划算?
- 代价一:漏掉没预压的文件
- 代价二:它会换掉你的ETag
- Vary漏了会发生什么?
- 只有爬虫会读的那几个文件,为什么反而最容易漏?
- 顺带纠正一个常见误解
- 现网的编码分布藏着一条边缘与源站的分界线
- 怎么配才算把这件事做完?
- 先自查:三条命令看清现状
- 按优先级排的动作清单
- 一份完整的参考配置
- 一个诚实的边界
- 常见问题解答
- 我已经开了gzip,为什么检测工具说通过但字节没少多少?
- gzip和Brotli该开哪个,还是两个都开?
- Brotli级别该选多少?
- 压缩会不会影响ETag和条件请求?
- 站点地图需要单独配压缩吗?
- 压缩能让我突破Googlebot的2MB抓取上限吗?
- 用了CDN是不是就不用管服务器上的压缩配置了?
- 为什么Accept-Encoding: *拿不到压缩版?
- 权威参考资料
摘要:压缩大概是SEO技术清单上最早被划掉的一项——检测工具说“已启用gzip”,这件事就算完了。但nginx的出厂默认值是
gzip_types text/html,也就是只压HTML这一种类型;gzip_comp_level默认1,是最低档;gzip_vary默认关闭。本文在真实nginx上把17种压缩配置乘16种Accept-Encoding写法乘6类文件跑了一遍,又量了85个线上站点216个URL的真实过线字节:一套典型资源不压是605 KB,出厂默认只降到83.4%,把类型清单补全直接降到25.4%——补类型的收益是调级别的25倍。另外测出Brotli最高级别每个请求要多花将近1秒CPU,只换来10%的体积。
先做一道很短的算术。
一个电商站的商品页,HTML 190 KB,主样式表84 KB,主脚本210 KB,站点地图2.4 MB。爬虫来一次,把前三个拿走;每天来一次拿站点地图。
如果这四类东西都压过,实际过线的大概是HTML 30 KB、CSS 14 KB、JS 24 KB、站点地图140 KB。如果只有HTML压了——这正是nginx不改任何配置、只写一行gzip on时的状态——那么过线的是30 KB加84 KB加210 KB加2.4 MB。
差出来的不是一点点,是2.6 MB对0.2 MB。
而这两种状态在任何一个在线检测工具上,都会显示为同一个结果:已启用gzip压缩,通过。
爬虫每次拿走的字节里,有多少本来不用传?
这个问题保哥没找到现成的答案,只能自己量一个基准出来。
取样是164个线上域名,其中85个能正常抓到首页。对每个站取四类URL——首页、一个深层内容页、robots.txt里声明的站点地图、页面上第一个静态资源,一共216个能同时拿到压缩版和未压缩版的测量点。
这里有个坑值得先说:量压缩收益不能用HTTP客户端库直接读响应体长度,因为它们默认会自动解压,你拿到的是解压后的字节数。本文第一遍就踩了,测出来“压缩后占比中位数100%”这种荒唐结果。得关掉自动解码,数真正过网线的那些字节。
基准:一次抓取的真实字节分布
| URL类型 | 样本 | 未压缩合计 | 实际过线合计 | 占比 | 压了的比例 |
|---|---|---|---|---|---|
| 首页 | 83 | 48.5 MB | 6.7 MB | 13.9% | 81/83 |
| 深层内容页 | 29 | 13.8 MB | 1.7 MB | 12.2% | 29/29 |
| 站点地图 | 66 | 17.8 MB | 1.0 MB | 5.7% | 56/66 |
| 静态资源 | 38 | 4.9 MB | 0.9 MB | 17.8% | 35/38 |
| 合计 | 216 | 85.0 MB | 10.3 MB | 12.1% | 201/216 |
顺带一提,这86%的压缩率是“同一份内容传得更省”,跟网页体积本身影响排名的那套说法不是一回事——后者讨论的是内容该不该那么大,这里只讨论既然这么大、传的时候该收多少费。
整体看,压缩把过线字节压到了原文的12.1%。这就是压缩在抓取预算上的分量——Google抓取预算优化的那份实操清单里说得很清楚,抓取容量上限计的是服务器为爬虫保持连接的总时长,而传1个字节和传8个字节的耗时不是一回事。
但表里还有两个数值得停一下。
第一个是站点地图那行的5.7%——它是全表压得最狠的一类,因为XML的结构重复度极高。实验台上造了一份1200条URL的站点地图,234 KB原文,最高级别压完只剩5.9 KB,2.5%。这类文件天然就是压缩的最佳受益者。
第二个数正好相反:66个站点地图里有10个完全没压,占15%。也就是说,压缩收益最大的那一类文件,恰恰是漏配率最高的那一类。这不是巧合,后面会讲原因。
那15个没压的,都是什么
216个测量点里有15个在浏览器的默认编码偏好下拿到的仍然是原文。绝大多数体积不大(几百字节到几KB,压不压差别有限),但有一个例外:一份application/xml的站点地图,202,507字节,一个字节没压。
这份文件如果压过,大概只剩5 KB到10 KB。
爬虫每来一次取一遍,一年下来的差额是三位数的MB——就为了一行没写进配置文件的MIME类型。
nginx出厂到底压了什么?
这一节是本文的核心。因为绝大多数“已启用gzip”的站点,实际状态就是出厂默认。
nginx的gzip模块文档把四个默认值写得清清楚楚,只是很少有人一次把它们看全:
| 指令 | 默认值 | 意味着什么 |
|---|---|---|
gzip_types | text/html | 只压HTML。CSS、JS、XML、JSON全部原样发 |
gzip_comp_level | 1 | 九档里的最低档 |
gzip_min_length | 20 | 20字节以上就压,这个默认值没问题 |
gzip_vary | off | 压了,但不告诉下游缓存“我是按编码分版本的” |
第一行是最要命的。gzip_types的默认值是text/html,而且文档特别注明HTML永远会被压缩,所以你写不写它都在里面。换句话说,只写一行gzip on;而不写gzip_types,等于只给HTML开了压缩。
把这四个默认值的代价量出来
实验台上造了六类文件,覆盖爬虫真正会遇到的形态:一个128 KB的HTML、一个60 KB的CSS、一个91 KB的JS、一个234 KB的站点地图、一个90 KB的JPEG、一个1.5 KB的小HTML,合计605,099字节。同一批文件放在不同配置的location下,用浏览器和爬虫的真实编码偏好各取一遍:
| 配置 | HTML | CSS | JS | 站点地图 | JPEG | 合计 | 占原文 |
|---|---|---|---|---|---|---|---|
| 完全不压 | 127,997 | 60,444 | 91,042 | 234,110 | 90,022 | 605,099 | 100.0% |
| 出厂默认(只压HTML,级别1) | 28,389 | 60,444 | 91,042 | 234,110 | 90,022 | 504,597 | 83.4% |
| 类型补全,级别仍是1 | 28,389 | 13,876 | 11,897 | 8,758 | 90,022 | 153,532 | 25.4% |
| 类型补全,级别6 | 20,964 | 10,753 | 10,345 | 6,870 | 90,022 | 139,527 | 23.1% |
| 类型补全,级别9 | 20,887 | 10,277 | 9,715 | 5,941 | 90,022 | 137,415 | 22.7% |
| Brotli(级别5) | 24,668 | 10,235 | 9,178 | 5,340 | 90,022 | 139,915 | 23.1% |
| 预压缩件(级别9) | 20,897 | 10,515 | 9,727 | 5,953 | 90,022 | 138,598 | 22.9% |
把第二行和第三行放一起看,这是本文最值得记住的一组数:
- 出厂默认 → 类型补全:83.4%降到25.4%,收益58个百分点
- 级别1 → 级别6:25.4%降到23.1%,收益2.3个百分点
把MIME类型清单写全,收益是调压缩级别的25倍。而网上绝大多数“nginx压缩优化”的文章,篇幅都花在讨论级别选几合适。
那一列纹丝不动的JPEG
上表里JPEG那一列在所有配置下都是90,022,因为没有任何一档把image/jpeg放进gzip_types。这是对的。
但为了确认代价,实验台上专门开了一个location把它放进去。结果是:
原文: 90,022 字节
gzip 之后: 90,070 字节 (大了 48 字节)
压完比原文大。MDN对这件事有直接说明:The data is already compressed, meaning a second round of compression will not reduce the transmitted data size, and may actually increase the size of the content in some cases. This is true for pre-compressed image formats (JPEG, for instance).
48字节不算什么,问题是你为这48字节付出了一次完整的压缩CPU开销。图片通常是站上数量最多的一类资源,把它们放进类型清单,等于给每一次图片请求都加了一道纯亏的工序。
一份可以直接抄的类型清单
gzip on;
gzip_comp_level 5;
gzip_min_length 256;
gzip_vary on;
gzip_proxied any;
gzip_types
text/plain text/css text/xml
application/javascript application/json application/xml
application/rss+xml application/atom+xml
image/svg+xml
application/manifest+json
text/markdown;
三点说明。text/html不用写,它永远在。image/svg+xml要写,SVG是文本,压缩比很高,但因为归在image/下经常被整类跳过。gzip_min_length从默认的20提到256,是因为几十字节的响应压完往往比原文大——gzip自己的头尾就要18字节。
Accept-Encoding里那些没人测过的写法
压缩要不要发生,取决于客户端在Accept-Encoding里说了什么。这个头看起来只有两三种写法,实际的边界比想象中乱得多。
实验台上用16种写法各打一遍,同一个文件、同一个开了gzip和Brotli的location:
| Accept-Encoding | 拿到的编码 | 下行字节 | 说明 |
|---|---|---|---|
gzip, deflate, br | br | 24,668 | 浏览器与爬虫的典型值 |
gzip | gzip | 23,539 | |
br | br | 24,668 | |
GZIP | gzip | 23,539 | 大小写不敏感 |
zstd, gzip | gzip | 23,539 | 不支持的编码被跳过,退到下一个 |
gzip;q=1.0, br;q=0.5 | br | 24,668 | 客户端明说更想要gzip,还是给了br |
identity | 无 | 127,997 | |
gzip;q=0 | 无 | 127,997 | 正确:q=0表示拒绝 |
* | 无 | 127,997 | 规范说*匹配任何编码 |
identity;q=0 | 无 | 127,997 | 客户端明说拒绝原文,照发原文 |
deflate | 无 | 127,997 | nginx不实现deflate编码 |
zstd | 无 | 127,997 | 需要额外模块 |
| (空值) | 无 | 127,997 |
四行加粗的,逐个说。
q值:写了也白写
gzip;q=1.0, br;q=0.5的意思是“我两个都能收,但更想要gzip”。nginx给的是br。换成br;q=1.0, gzip;q=0.5,给的还是br。
nginx在选编码时不看q值,只看谁在配置里排在后面。ngx_brotli作为一个独立的过滤模块挂在gzip之后,于是只要客户端接受br,br就赢。这个行为在两边的文档里都没写,但它决定了一件很实际的事:你没法通过调整客户端偏好来让某一类客户端拿gzip。要控制这件事只能在服务端关掉一个。
*:规范说匹配一切,nginx当没看见
MDN对*的定义是:Matches any content encoding not already listed in the header.按这个语义,Accept-Encoding: *应该拿到压缩版。nginx给的是原文。
这条落到现网有多大规模?用Accept-Encoding: *对216个测量点各打了一遍:
| 请求头 | 拿到压缩版的比例 |
|---|---|
gzip | 203/218=93.1% |
br | 112/218=51.4% |
* | 41/218=18.8% |
八成以上的服务器不认这个通配符。所以如果你在写爬虫、监控探针或者健康检查,别用*——它看起来最省事,实际会让你拿到未压缩的响应,并且在你的带宽账单上体现出来。
identity;q=0:一个大家都不实现的角落
MDN写得很明确:As long as the identity;q=0 or *;q=0 directives do not explicitly forbid the identity value that means no encoding, the server must never return a 406 Not Acceptable error.反过来说,一旦显式写了identity;q=0,就是明确禁止了原文,服务器要么给压缩版、要么406。
nginx给的是原文。这个角落几乎没有实际影响(没有客户端会这么写),但它是个很好的提醒:内容协商的规范面积远大于实现面积,那些没人走的分支上基本都是默认行为。
deflate:浏览器发了二十年的一个空头
所有主流浏览器的Accept-Encoding里都带着deflate,而nginx压根没有实现这个编码。它既不在gzip模块里,也没有独立模块。Accept-Encoding: deflate拿到的一定是原文。
这不是nginx的问题——deflate在历史上有过两种互不兼容的封装方式,实现分歧大到没人愿意碰。它今天的作用就是在请求头里占七个字节。
Brotli一定比gzip小吗?
这个问题保哥差点答错,过程值得说一下。
实验台第一轮用的是合成HTML——脚本拿一个词表随机拼出来的128 KB页面。结果是gzip压到23,539,Brotli压到24,668,Brotli反而大了4.8%。当时差点就把“Brotli不一定更小”写成结论。
幸好换了真实文件复测。从线上拉了四个真东西:一个485 KB的首页、一个515 KB的文章页、一个305 KB的站点地图、一个24 KB的纯文本清单,同一批配置再跑一遍:
| 文件(原文) | gzip级别1 | gzip级别6 | gzip级别9 | Brotli级别6 | Brotli级别11 |
|---|---|---|---|---|---|
| 首页485,947 B | 74,995 | 62,187 | 61,139 | 52,324 | 45,498 |
| 文章页515,230 B | 105,284 | 89,913 | 88,997 | 71,890 | 64,504 |
| 站点地图304,607 B | 50,426 | 41,017 | 40,498 | 35,771 | 31,293 |
| 纯文本清单23,774 B | 10,993 | 10,431 | 10,416 | 9,483 | 8,702 |
真实内容上Brotli每一格都赢,而且赢得不少。
合成文本那个反常结果的原因是:Brotli自带一部内置字典,里面是从真实网页里统计出来的高频片段——HTML标签、常见属性、英文常用词。随机拼出来的文本正好把这个优势全废掉了。
这是一条方法论上的教训,比结论本身更值钱:凡是拿合成数据测压缩比的,测出来的是压缩算法对合成数据的表现,跟你的站没关系。
现网复核:Brotli赢在93%的URL上
实验台是四个文件,还是太少。保哥对线上216个测量点里同时能给gzip和br两个版本的109个,分别用Accept-Encoding: gzip和Accept-Encoding: br各取一遍,量真实过线字节:
| 指标 | 结果 |
|---|---|
| 同一批URL全部走gzip | 6,822,287 B |
| 同一批URL全部走Brotli | 5,548,651 B |
| Brotli相对gzip再省 | 18.7% |
| 逐个URL比,Brotli更小的 | 101/109=93% |
所以Brotli该开,这没有疑问。有疑问的是开在哪一档。
压缩级别的边际收益,在哪一档断掉?
压缩级别是一笔用CPU换带宽的买卖。带宽那一侧前面已经量了,现在量CPU那一侧。
做法很朴素:同一个515 KB的文章页,在每种配置下连打30次,记总耗时,再减掉不压缩时的基线,得到每个请求净增的压缩时间。
| 配置 | 压完的体积 | 占原文 | 每请求净增耗时 |
|---|---|---|---|
| gzip级别1 | 105,284 B | 20.4% | 约20 ms |
| gzip级别6 | 89,913 B | 17.5% | 约31 ms |
| gzip级别9 | 88,997 B | 17.3% | 约52 ms |
| Brotli级别6 | 71,890 B | 14.0% | 约33 ms |
| Brotli级别11 | 64,504 B | 12.5% | 约999 ms |
最后一行不是笔误。Brotli最高级别在这份515 KB的文档上,每个请求要多花将近一整秒的CPU时间,换来的是相对级别6再小10.3%。
体积省10%,耗时涨30倍。
为什么这一秒对SEO格外贵
如果这只是一笔服务器成本,还可以商量。但它同时撞在抓取预算的另一条规则上。
Google那份抓取预算文档里对抓取容量上限的浮动规则写得很直接:
If the site slows down (latency increases or response times become longer)…the limit goes down and Google crawls less.
反过来,响应时间保持稳定或者变快,这个上限才会往上走。
压缩发生在服务器准备好内容之后、发出第一个字节之前,所以它整个计入首字节时间。一秒的压缩延迟,直接变成一秒的首字节时间。
于是出现一个自我拆台的局面:你为了少传字节而选了最高压缩级别,结果因为响应变慢,Google给你的抓取额度反而降了。省下的10%体积,换来的是更少的抓取次数。这跟TTFB怎么优化才不白费是同一条链上的事——首字节时间同时挂在体验信号和抓取速率两头。
结论:级别的正确用法是分场景,不是分级别
| 场景 | 建议 | 理由 |
|---|---|---|
| 动态内容运行时压缩 | gzip 4–6或Brotli 4–5 | 每次请求都要付CPU,必须控制在几十毫秒内 |
| 构建期预压缩的静态件 | gzip 9、Brotli 11随便用 | 压一次,发无数次,CPU成本摊到零 |
| 站点地图这类定时生成的 | 生成时就压好落盘 | 它一天才变一次,没道理每次请求现压 |
换句话说,Brotli 11不是“更好的压缩级别”,它是另一种东西——一个只能离线用的档位。把它写进运行时配置,是本文见过的最贵的一行配置。
预压缩为什么几乎总是更划算?
顺着上一节往下推,预压缩看起来是完美方案:构建期用最高级别压好,运行时直接把.gz或.br文件发出去,体积最小、CPU为零。
实验台上的数字支持这个判断:
| 方式 | HTML体积 | 六类文件合计 | 运行时CPU |
|---|---|---|---|
| 运行时gzip级别5 | 23,539 | 142,613 | 每请求都付 |
| 预压缩件 级别9 | 20,897 | 138,598 | 零 |
体积小11.2%,CPU降到零。但预压缩有两个代价,都得先知道。
代价一:漏掉没预压的文件
纯预压缩方案(gzip_static on而不同时开gzip on)只会发磁盘上确实存在的.gz。实验台上那个1.5 KB的小HTML没有预压件,于是在纯预压缩配置下拿到的是1,484字节原文,而运行时压缩配置下是573字节。
正解是两个一起开,让运行时压缩兜底:
gzip_static on; # 有预压件就发预压件
gzip on; # 没有就现压
gzip_comp_level 5;
gzip_types ...;这组配置在实验台上给出了全表最小的合计值(137,704字节)。
代价二:它会换掉你的ETag
这一条更隐蔽,而且直接和抓取预算的另一半冲突。
运行时压缩时,nginx保持ETag的值不变,只加个W/前缀标成弱验证器;而gzip_static发的是磁盘上另一个文件,ETag按那个文件的属性重算,变成一个完全无关的值:
| 配置 | 不压时的ETag | 要gzip时的ETag | 拿未压缩版ETag回问 |
|---|---|---|---|
gzip on | "6a6a35c5-1f3fd" | W/"6a6a35c5-1f3fd" | 304 |
gzip_static on | "6a6a35c5-1f3fd" | "6a6a35c5-51a1" | 200 |
也就是说,你用预压缩省下的那11%体积,可能被“条件请求失效导致的整页重发”一口吃掉。条件请求那一篇里量过这件事的现网规模:54.1%的URL在压缩前后ETag不一样,其中32.1%会因此白下载一次全文。
两件事要合起来看:压缩省的是“这一次传多少”,条件请求省的是“这一次传不传”。后者的收益是前者的十几倍,所以当两者冲突时,优先保后者。
Vary漏了会发生什么?
前面表里那个gzip_vary off的默认值,值得单独一节。
MDN对Vary的说明是:Including a Vary header ensures that responses are separately cached based on the headers listed in the Vary field.如果服务器按Accept-Encoding发了不同版本却不声明Vary,中间缓存会把压缩版和未压缩版当成同一个东西,然后把错的那个发给下一个客户端。
实验台上确认了默认值:
| 配置 | 压缩时的Vary |
|---|---|
gzip on;(没写gzip_vary) | 没有 |
gzip on; gzip_vary on; | Vary: Accept-Encoding |
这行头本身还管着别的事——响应头层面那套SEO机制里,Vary同时影响着缓存分版本与内容协商的正确性,这里只谈它跟压缩相关的那一半。
好消息是现网这一项做得不错。203个压了的测量点里,带Vary的有178个=87.7%,其中明确含Accept-Encoding的171个=84.2%。这大概是因为主流CDN会替你补上这行头。
但如果你是自建nginx直出、前面没有CDN,这一行必须自己写。MDN还提了一句跟另一半直接相关的:The same Vary header value should be used on all responses for a given URL, including 304 Not Modified responses.——304里也得带。而nginx的304恰好会把它丢掉。
只有爬虫会读的那几个文件,为什么反而最容易漏?
回到开头那个数:66份站点地图里有10份完全没压,占15%,是四类URL里漏配率最高的一类。而它们恰好是压缩比最高的一类(整体5.7%,实验台上那份合成站点地图是2.5%)。
收益最高、漏配率也最高,这两件事同时成立不是巧合。原因有三层:
- 它的MIME类型是
application/xml或text/xml,不在任何“默认压缩”的清单里,必须手写进gzip_types。 - 没人用浏览器看它。前端性能工具、Lighthouse、体验监控,扫的都是用户会访问的页面,站点地图从不在采样范围内。
- 它常常不由Web服务器直出。很多站的站点地图是应用动态生成的,或者由插件走另一条路径吐出来,那条路径上的压缩配置和站点其余部分完全是两套。
如果你的站点地图本身就很大,分片策略会直接放大这个差额,百万SKU的分片与lastmod诚实度那篇里的每一个分片,都是爬虫要单独取一次的文件。
同一类问题也发生在llms.txt、robots.txt、各种feed和JSON接口上——凡是只有机器读的端点,都在人的观测范围之外。这条经验可以直接推广:一个端点被漏配的概率,跟“有多少人会用浏览器打开它”成反比。
顺带纠正一个常见误解
有人会想:既然站点地图能压到2.5%,那是不是可以往里塞更多URL?或者反过来——既然页面能压到12%,Googlebot那个2 MB的抓取上限是不是就宽松了?
不是。Google在Googlebot那份文档里写得很明白:Googlebot crawls the first 2MB of a supported file type,并且补了一句关键的——the file size limit is applied on the uncompressed data.
那个上限按解压后的数据算。压缩省的是传输,不是配额。站点地图的50,000条URL上限和50 MB未压缩上限同理,都是按解压后算的。这跟Googlebot那个2MB抓取上限是同一件事的两个侧面:压缩帮你省抓取时间,但帮不了你突破内容体积的天花板。
现网的编码分布藏着一条边缘与源站的分界线
把216个测量点按实际拿到的编码归类,出现了一个一开始没料到的分布:
| URL类型 | 样本 | Brotli | gzip | 不压 |
|---|---|---|---|---|
| 首页 | 83 | 45(54%) | 36(43%) | 2 |
| 深层内容页 | 29 | 3(10%) | 26(90%) | 0 |
| 站点地图 | 66 | 33(50%) | 23(35%) | 10 |
| 静态资源 | 38 | 11(29%) | 24(63%) | 3 |
首页有54%走Brotli,深层内容页只有10%。同一批站点、同一个请求头,差别为什么这么大?
最可能的解释是缓存命中率,而这正是边缘缓存的回源与TTL分层那套配置在决定的事。首页是全站访问量最高的URL,几乎必然常驻在CDN边缘节点上,而边缘节点普遍支持Brotli;深层内容页长尾、命中率低,请求多半要回源,源站那一层往往只配了gzip。
这条推断不算铁证(样本里深层页只有29个),但它指向一个可操作的检查动作:别只测首页。首页的压缩表现是CDN的表现,深层页的压缩表现才是你自己那台服务器的表现。而爬虫花掉的绝大部分抓取预算,正是在深层页上。
这也解释了另一件事:为什么很多站在检测工具上一路绿灯——那些工具默认测的就是首页。
怎么配才算把这件事做完?
先自查:三条命令看清现状
# 1) 深层内容页(不是首页!)拿到什么编码
curl -sI -H 'Accept-Encoding: gzip, deflate, br' \
https://你的域名/某个文章页 | grep -i 'content-encoding\|vary'
# 2) 站点地图有没有压
curl -s -o /dev/null -w 'ce=%{content_type} size=%{size_download}\n' \
-H 'Accept-Encoding: gzip, deflate, br' https://你的域名/sitemap.xml
# 3) 同一个URL压与不压的真实字节差
for ae in identity 'gzip, deflate, br'; do
curl -s -o /dev/null -w "$ae -> %{size_download}\n" \
-H "Accept-Encoding: $ae" --compressed-no-var https://你的域名/某个文章页
done第2条如果返回的体积和不带压缩头时一样,说明XML没进类型清单——这是本文实测中漏配率最高的一格。
按优先级排的动作清单
| 优先级 | 动作 | 预期收益 |
|---|---|---|
| 1 | 把CSS、JS、XML、JSON、SVG补进gzip_types | 整体字节从83.4%降到25.4% |
| 2 | 确认站点地图、feed、机读端点走的是同一套压缩配置 | 单个文件常常是几十到几百KB的差额 |
| 3 | 开gzip_vary on | 防止中间缓存把版本发错 |
| 4 | 开Brotli,级别4到5 | 相对gzip再省约18.7% |
| 5 | 级别从默认1提到4到6 | 再省约2.3个百分点 |
| 6 | 静态件走构建期预压缩,级别拉满 | 体积再降11%,CPU归零 |
| — | 别做:每请求多花约1秒 | |
| — | gzip_types | 别做:压完更大,还白付CPU |
一份完整的参考配置
gzip on;
gzip_static on; # 有预压件优先发预压件
gzip_comp_level 5;
gzip_min_length 256;
gzip_vary on;
gzip_proxied any;
gzip_types
text/plain text/css text/xml text/markdown
application/javascript application/json application/xml
application/rss+xml application/atom+xml
application/manifest+json
image/svg+xml;
brotli on;
brotli_static on;
brotli_comp_level 5;
brotli_min_length 256;
brotli_types
text/plain text/css text/xml text/markdown
application/javascript application/json application/xml
application/rss+xml application/atom+xml
application/manifest+json
image/svg+xml;两套类型清单要写一样,否则会出现“只有接受gzip的客户端才拿得到压缩版CSS”这种非常难查的偏科。
一个诚实的边界
压缩这件事的收益上限是有限的,保哥觉得有必要说清楚。
本文量出来的最大单笔收益——把类型清单补全——把一套典型资源从605 KB降到153 KB,省了452 KB。听起来很多,但对照一下另一半:如果这些URL的内容没变,条件请求能把同样一次抓取从153 KB降到不到200字节。
压缩省掉的是一次抓取里的百分之七十几,条件请求省掉的是整整一次抓取。
所以这两件事的正确顺序是:先把条件请求做对,再把压缩做满。如果时间只够做一件,做前者。压缩的真正价值在于那些确实变了、非传不可的响应——对它们来说,压缩是唯一能省的东西。
另外,如果你的站只有几百个页面,这两件事的实际收益都接近于零,爬虫本来就抓得完。规模才是这类优化的前提,这一点在抓取预算优化那份清单里也是同样的口径。
常见问题解答
我已经开了gzip,为什么检测工具说通过但字节没少多少?
大概率是gzip_types没配。nginx这个指令的默认值是text/html,只写一行gzip on;等于只压HTML,CSS、JS、XML、JSON全部原样发。实验台上这个差别是整体字节83.4%对25.4%。检测工具通常只看首页HTML有没有Content-Encoding,看到了就打勾。
gzip和Brotli该开哪个,还是两个都开?
两个都开,让客户端自己挑。现网实测里同时能给两种编码的109个URL,Brotli在其中93%上更小,整体再省18.7%。但注意nginx不看客户端的q值偏好——只要客户端接受Brotli就一定给Brotli,你没法通过请求头让它退回gzip。
Brotli级别该选多少?
运行时压缩选4到5。实测级别11在一份515 KB的文档上每个请求要多花约999毫秒CPU,只换来相对级别6再小10.3%的体积,而这一秒会整个计入首字节时间,反过来压低Google给的抓取速率。级别11只适合构建期预压缩,那时候压一次发无数次,CPU成本可以忽略。
压缩会不会影响ETag和条件请求?
会。运行时压缩只是把ETag标成弱验证器(加W/前缀),值不变,不影响条件请求;但gzip_static发的是磁盘上另一个文件,ETag完全不同,客户端拿未压缩版的ETag回问会拿到200全文。现网实测54.1%的URL在压缩前后ETag不一样。两者冲突时优先保条件请求,因为它省的字节多一个数量级。
站点地图需要单独配压缩吗?
需要检查。它的MIME类型是application/xml或text/xml,不在默认清单里;而且很多站的站点地图是应用生成的,走的路径和站点其余部分不是一套配置。本次普查里66份站点地图有10份完全没压,是四类URL中漏配率最高的,而它又是压缩比最高的一类(整体压到5.7%)。
压缩能让我突破Googlebot的2MB抓取上限吗?
不能。Google的文档写明the file size limit is applied on the uncompressed data,2MB是按解压后的数据算的。压缩省的是传输时间和带宽,不是内容配额。站点地图的50,000条URL和50 MB上限同理。
用了CDN是不是就不用管服务器上的压缩配置了?
不能这么假设。本次普查里首页有54%走Brotli,深层内容页只有10%——首页常驻边缘节点,深层页多半要回源,回源那一层是你自己的配置。而爬虫的抓取预算绝大部分花在深层页上。检查压缩配置时一定要测深层页,别只测首页。
为什么Accept-Encoding: *拿不到压缩版?
规范上*表示匹配任何未列出的编码,但绝大多数实现不认。实测216个测量点里用*只有18.8%拿到压缩版,而用gzip是93.1%。写监控探针、健康检查或者自建爬虫时,一定要显式写gzip, deflate, br,否则你会一直在下载未压缩的响应。
权威参考资料
本文标题:《nginx出厂只压HTML这一种类型,其余的字节全额计入了SEO抓取预算》
本文链接:https://zhangwenbao.com/content-encoding-negotiation-gzip-brotli-crawl-budget-seo.html
版权声明:本文原创,转载与引用请注明作者与原文链接。许可协议: CC BY 4.0
← 上一篇
做SEO算抓取预算,发现服务器把没改过的页面对爬虫重发了几百遍下一篇 →
没有了