nginx出厂只压HTML这一种类型,其余的字节全额计入了SEO抓取预算

nginx出厂只压HTML这一种类型,其余的字节全额计入了SEO抓取预算
张文保 35 分钟阅读 1,633 阅读
本文目录
  1. 爬虫每次拿走的字节里,有多少本来不用传?
  2. 基准:一次抓取的真实字节分布
  3. 那15个没压的,都是什么
  4. nginx出厂到底压了什么?
  5. 把这四个默认值的代价量出来
  6. 那一列纹丝不动的JPEG
  7. 一份可以直接抄的类型清单
  8. Accept-Encoding里那些没人测过的写法
  9. q值:写了也白写
  10. *:规范说匹配一切,nginx当没看见
  11. identity;q=0:一个大家都不实现的角落
  12. deflate:浏览器发了二十年的一个空头
  13. Brotli一定比gzip小吗?
  14. 现网复核:Brotli赢在93%的URL上
  15. 压缩级别的边际收益,在哪一档断掉?
  16. 为什么这一秒对SEO格外贵
  17. 结论:级别的正确用法是分场景,不是分级别
  18. 预压缩为什么几乎总是更划算?
  19. 代价一:漏掉没预压的文件
  20. 代价二:它会换掉你的ETag
  21. Vary漏了会发生什么?
  22. 只有爬虫会读的那几个文件,为什么反而最容易漏?
  23. 顺带纠正一个常见误解
  24. 现网的编码分布藏着一条边缘与源站的分界线
  25. 怎么配才算把这件事做完?
  26. 先自查:三条命令看清现状
  27. 按优先级排的动作清单
  28. 一份完整的参考配置
  29. 一个诚实的边界
  30. 常见问题解答
  31. 我已经开了gzip,为什么检测工具说通过但字节没少多少?
  32. gzip和Brotli该开哪个,还是两个都开?
  33. Brotli级别该选多少?
  34. 压缩会不会影响ETag和条件请求?
  35. 站点地图需要单独配压缩吗?
  36. 压缩能让我突破Googlebot的2MB抓取上限吗?
  37. 用了CDN是不是就不用管服务器上的压缩配置了?
  38. 为什么Accept-Encoding: *拿不到压缩版?
  39. 权威参考资料

摘要:压缩大概是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类型样本未压缩合计实际过线合计占比压了的比例
首页8348.5 MB6.7 MB13.9%81/83
深层内容页2913.8 MB1.7 MB12.2%29/29
站点地图6617.8 MB1.0 MB5.7%56/66
静态资源384.9 MB0.9 MB17.8%35/38
合计21685.0 MB10.3 MB12.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_typestext/html只压HTML。CSS、JS、XML、JSON全部原样发
gzip_comp_level1九档里的最低档
gzip_min_length2020字节以上就压,这个默认值没问题
gzip_varyoff压了,但不告诉下游缓存“我是按编码分版本的”

第一行是最要命的。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下,用浏览器和爬虫的真实编码偏好各取一遍:

配置HTMLCSSJS站点地图JPEG合计占原文
完全不压127,99760,44491,042234,11090,022605,099100.0%
出厂默认(只压HTML,级别1)28,38960,44491,042234,11090,022504,59783.4%
类型补全,级别仍是128,38913,87611,8978,75890,022153,53225.4%
类型补全,级别620,96410,75310,3456,87090,022139,52723.1%
类型补全,级别920,88710,2779,7155,94190,022137,41522.7%
Brotli(级别5)24,66810,2359,1785,34090,022139,91523.1%
预压缩件(级别9)20,89710,5159,7275,95390,022138,59822.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, brbr24,668浏览器与爬虫的典型值
gzipgzip23,539
brbr24,668
GZIPgzip23,539大小写不敏感
zstd, gzipgzip23,539不支持的编码被跳过,退到下一个
gzip;q=1.0, br;q=0.5br24,668客户端明说更想要gzip,还是给了br
identity127,997
gzip;q=0127,997正确:q=0表示拒绝
*127,997规范说*匹配任何编码
identity;q=0127,997客户端明说拒绝原文,照发原文
deflate127,997nginx不实现deflate编码
zstd127,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个测量点各打了一遍:

请求头拿到压缩版的比例
gzip203/218=93.1%
br112/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级别1gzip级别6gzip级别9Brotli级别6Brotli级别11
首页485,947 B74,99562,18761,13952,32445,498
文章页515,230 B105,28489,91388,99771,89064,504
站点地图304,607 B50,42641,01740,49835,77131,293
纯文本清单23,774 B10,99310,43110,4169,4838,702

真实内容上Brotli每一格都赢,而且赢得不少。

合成文本那个反常结果的原因是:Brotli自带一部内置字典,里面是从真实网页里统计出来的高频片段——HTML标签、常见属性、英文常用词。随机拼出来的文本正好把这个优势全废掉了。

这是一条方法论上的教训,比结论本身更值钱:凡是拿合成数据测压缩比的,测出来的是压缩算法对合成数据的表现,跟你的站没关系。

现网复核:Brotli赢在93%的URL上

实验台是四个文件,还是太少。保哥对线上216个测量点里同时能给gzip和br两个版本的109个,分别用Accept-Encoding: gzipAccept-Encoding: br各取一遍,量真实过线字节:

指标结果
同一批URL全部走gzip6,822,287 B
同一批URL全部走Brotli5,548,651 B
Brotli相对gzip再省18.7%
逐个URL比,Brotli更小的101/109=93%

所以Brotli该开,这没有疑问。有疑问的是开在哪一档。

压缩级别的边际收益,在哪一档断掉?

压缩级别是一笔用CPU换带宽的买卖。带宽那一侧前面已经量了,现在量CPU那一侧。

做法很朴素:同一个515 KB的文章页,在每种配置下连打30次,记总耗时,再减掉不压缩时的基线,得到每个请求净增的压缩时间。

配置压完的体积占原文每请求净增耗时
gzip级别1105,284 B20.4%约20 ms
gzip级别689,913 B17.5%约31 ms
gzip级别988,997 B17.3%约52 ms
Brotli级别671,890 B14.0%约33 ms
Brotli级别1164,504 B12.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级别523,539142,613每请求都付
预压缩件 级别920,897138,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/xmltext/xml,不在任何“默认压缩”的清单里,必须手写进gzip_types
  • 没人用浏览器看它。前端性能工具、Lighthouse、体验监控,扫的都是用户会访问的页面,站点地图从不在采样范围内。
  • 它常常不由Web服务器直出。很多站的站点地图是应用动态生成的,或者由插件走另一条路径吐出来,那条路径上的压缩配置和站点其余部分完全是两套。

如果你的站点地图本身就很大,分片策略会直接放大这个差额,百万SKU的分片与lastmod诚实度那篇里的每一个分片,都是爬虫要单独取一次的文件。

同一类问题也发生在llms.txtrobots.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类型样本Brotligzip不压
首页8345(54%)36(43%)2
深层内容页293(10%)26(90%)0
站点地图6633(50%)23(35%)10
静态资源3811(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的差额
3gzip_vary on防止中间缓存把版本发错
4开Brotli,级别4到5相对gzip再省约18.7%
5级别从默认1提到4到6再省约2.3个百分点
6静态件走构建期预压缩,级别拉满体积再降11%,CPU归零
运行时用Brotli 11别做:每请求多花约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/xmltext/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

继续阅读
发表评论
分享到微信 或在下方手动填写
支持 Ctrl + Enter 提交