vary header 实测 27 个首页:27 个全在压缩,8 个从没声明过

vary header 这一行决定共享缓存按什么键存你的页面。2026-09-07 抓的 27 个首页里,27 个全部返回了压缩正文,却只有 19 个在里面写了 Accept-Encoding —— 8 个站做了内容协商没声明。整个面板 0 个发 Vary: User-Agent,还有 4 个站发出来的这一行在自我重复。

抓取与收录8 分钟读完817 次阅读
vary header 实测 27 个首页:27 个全在压缩,8 个从没声明过

实测 · 2026-09-07 · 27 个首页 · 每站一次请求 · 谁声明了「是什么改变了这份响应」

样本 / 口径:2026-09-07 当天对 30 个首页各发一次 GET,桌面 Chrome UA,跟随跳转,不执行 JavaScript,出口在日本。stackoverflow.com、medium.com、www.reddit.com 三个站返回 403,剔除后纳入 27 个。响应头按收到的原样记录,重复的行不合并。

27 个站的首页全部返回了压缩过的正文,但只有 19 个在 vary header 里写了 Accept-Encoding——也就是说 8 个站做了内容协商却没声明这件事。其中 4 个站一条 Vary 都没发,另外 4 个发了,但里面只有自家框架的路由字段。整个面板没有一个站发 Vary: User-Agent

出海站为什么会撞上这一行

这一行平时不用管,直到你在源站前面挂了一层 CDN。出海站的典型结构是三段:源站(Vercel、Netlify 或自建)、中间一层 CDN(Cloudflare、CloudFront、阿里云 CDN),再到海外用户。压缩可能在源站做,也可能在 CDN 做;Vary 这一行同样可能被两层各写一遍。

问题不在于哪一层写,而在于最后送到缓存面前的那一行说了什么。缓存拿它决定「这份存下来的响应,能不能直接给下一个人」。写少了,不同请求头的人可能共用同一份;写多了,缓存被切碎,命中率往下掉。

症状多半是什么
命中率偏低Vary 里塞了每人都不同的字段
压缩没声明两层各写一遍,后写的把前面覆盖了
手机版没生效同址分流却没声明按 UA 变

怎么测的

每个站一次 GET,报桌面 Chrome UA 而不是 Googlebot,跟随跳转,整套响应头落盘。统计时头名统一转小写,值按逗号切开,每个字段名单独计一个。重复的 header 行按原样保留——发两行 Vary 和发一行里写两个名字,是两件不同的事。

# 同样两行,拿去查自己的站
curl -sI -H 'Accept-Encoding: br, gzip' https://example.com/ \
  | grep -i -E '^(vary|content-encoding):'

# 有 content-encoding、vary 里却没有 accept-encoding = 下面数的那 8 个

三条局限,中间那条最要紧。一次请求、只取首页,所以这是一个网址的快照,不是一个站的体检。一次请求也看不出前面那层缓存到底存了什么、给第二个访问者返回了什么——本篇记录的是「声明」,不是「行为」。另外我们没有换不同的 Accept-Encoding 再请求一遍去看正文变不变,所以那 8 个站可能对每个人都发得好好的,它们只是没把这件事说出来。

27 个全在压缩,8 个没声明

压缩在这个面板上是满的:27 个站全部返回了 Content-Encoding,16 个 Brotli、11 个 gzip。分母没有歧义,所以 19 这个数可以直接按 27 读。每一份响应都有一部分是根据请求头选出来的,其中 19 份说了。

2026-09-07站数
返回压缩正文27 / 27
发了 Vary23 / 27
写了 Accept-Encoding19 / 27
压了但没写8 / 27
发 Vary: User-Agent0 / 27
发 Vary: *0 / 27

这 8 个站分成两组,长得完全不一样。4 个站一条 Vary 都没有:about.gitlab.com、astro.build、stripe.com、www.wikipedia.org。另外 4 个——nextjs.org、react.dev、supabase.com、vercel.com——发了 Vary,里面正好四个字段,全是同一个框架的前端路由用的:rscnext-router-state-treenext-router-prefetchnext-router-segment-prefetch。正文是压缩的。压缩不在名单里。

RFC 9110 对这个字段的定义写得很直白:Vary 这一行「describes what parts of a request message, aside from the method and target URI, might have influenced the origin server's process for selecting the content of this response」(RFC 9110 §12.5.5,2026-09-07 访问)。标准自己举的例子正好就是这 8 个站的处境:带 Vary: accept-encoding, accept-language 的响应,意思是源站「might have used the request's Accept-Encoding and Accept-Language header fields (or lack thereof) as determining factors while choosing the content for this response」。

它换来的东西是写成缓存的硬性义务的。列了字段名,就等于告诉缓存「MUST NOT use this response to satisfy a later request unless the later request has the same values for the listed header fields as the original request」。同一段还有一句更短的:「Vary expands the cache key required to match a new request to the stored cache entry.」

Vary 不是在描述你的服务器,是在给你和读者之间的每一层缓存下指令。

标准允许不发,但那是一个要自己拿主意的取舍

标准同时写了什么时候该发:源站「SHOULD generate a Vary header field on a cacheable response when it wishes that response to be selectively reused for subsequent requests」。它也允许省略,并且把代价写出来了——Vary「might be elided when an origin server considers variance in content selection to be less significant than Vary's performance impact on caching」。

这句话是给「Vary 会切碎缓存」这件事留的口子,是一个真实的工程选择。但那 4 个只发路由字段的站是不是有意这么选的,一次请求判不出来。

23 个 vary header,11 种写法

发了 Vary 的 23 个站里,字段名的组合有 11 种。最常见的是最朴素的那种:9 个站只写 Accept-Encoding,别的什么都没有。再往下就散了。

写法站数例子
只有压缩9www.netlify.com
只有路由字段4vercel.com
压缩加一个6www.nytimes.com
压缩加路由2linear.app
一长串混合2github.com

带框架路由字段的一共 6 个站:nextjs.org、react.dev、supabase.com、vercel.com、linear.app、www.figma.com。其中 2 个同时写了 Accept-Encoding,所以路由字段本身不是问题——linear.app 一行写了 11 个字段名,Accept-Encoding 就在里面。

另有 3 个站把 CDN 或自家应用的私有字段放进了缓存键:www.bbc.com 的 X-BBC-Edge-Scheme、www.nytimes.com 的 Fastly-SSL、www.theverge.com 的 x-user-state。这些都不是搜索引擎爬虫会发的请求头。它们只对认识自己的那层基础设施起作用,在别处是空转。

4 个站发出来的这一行在自我重复

有 4 个首页发出来的 Vary 值里同一个字段名出现了不止一次,或者干脆拆成了两行。这本身无害——字段名匹配不区分大小写,重复一次不会让缓存键更宽——但它是一个很稳的信号:不止一层在写同一个头。

收到的是什么
www.figma.comrsc,Accept-Encoding,Accept-Encoding
www.theverge.comAccept-Encoding, x-user-state, x-user-state
github.com一行 11 个名字,X-Requested-With 两次
railway.com两行:Accept,然后 accept-encoding

railway.com 那个最典型。两行的大小写还不一样,从外面看,这就是源站和边缘各写各的样子。出海站三层结构里最常见的坑就是这个:你在 CDN 后台配了一遍,源站框架又自己发了一遍,谁最后写赢了要靠抓一次包才知道。

没有一个站按 UA 分流,但有一个站按设备分了

27 个站里发 Vary: User-Agent 的是 0 个。这是本次最干净的一个结果,而且它跟 Google 的建议是一致的,不是相反。同址分流(一个网址按设备返回不同 HTML)才需要这个字段。Google 对它的描述是:这种配置「relies on user-agent sniffing and the Vary: user-agent HTTP response header to serve a different version of the HTML to different devices」,而且明说「Google recommends Responsive Web Design because it's the easiest design pattern to implement and maintain」(Mobile-first Indexing Best Practices,Google Search Central,2026-09-07 访问)。

所以这一列是空的,正是一个响应式面板该有的样子。有一个站从另一条路进来了:www.notion.com 发的是 X-Device-Class, accept-encoding,它按设备类别分,用的是自家边缘打的字段,不是原始 UA 串。我们没有以手机身份请求过这个首页,所以它会返回什么,说不出来。

这对你意味着什么

检查只有两行,真要改也是改一行配置,不是改代码。别默认你那套栈已经处理好了,先拿自己的站跑一遍。

  1. 请求自己的首页,只打印 Content-EncodingVary 两行。前者有、后者里没有 Accept-Encoding,你就在那 8 个里。
  2. 看一眼源站前面有没有东西在缓存这份响应。没有共享缓存时,少写这个字段不花什么代价;能出事的是那层共享缓存把压缩过的正文给了没要求压缩的客户端。
  3. 把你确实发出去的那串字段名念一遍。如果里面是框架的路由字段、却没有你 CDN 真正在协商的那个,说明两层在抢着写这一行。
  4. 不做同址分流就别加 User-Agent。响应式站加了只会白白把缓存切碎,本次 27 个站没有一个发它。
  5. 别用 Vary: *。标准写明代理「MUST NOT generate」它,而且它等于告诉每一层缓存:任何存下来的副本都不能直接复用。

这篇归在抓取与收录而不是性能,是因为 Vary 决定了共享缓存把哪一份存货交给下一个请求者,而爬虫就是请求者之一。这批站到底用了哪种压缩,数在 27 个首页的 content-encoding 实测;User-Agent 那一列为什么该是空的,方法在 mobile first indexing 怎么查。想直接看 AI 爬虫来要你的页面时拿到了什么,用 AI 爬虫检测

常见问题

你们是怎么测的

2026-09-07 当天,每个首页一次 GET,桌面 Chrome UA,跟随跳转,出口在日本,响应头按收到的原样存盘、重复行不合并。统计时值按逗号切开、头名转小写。30 个站里 3 个返回 403,分析前剔除,剩 27 个。全程没有执行 JavaScript,也没有换请求头再发第二次。

Vary 配错了会影响 SEO 吗

不作为排名输入,Google 也没有任何文档把它写成排名因素。它是从投递这一侧起作用的:这一行决定共享缓存什么时候可以复用存货,而 Google 只在一种配置里点过它的名——同址分流。响应式站的老实答案是,多数首页需要写的只有 Accept-Encoding 一个。

Cloudflare 会不会自己处理 Accept-Encoding

这次没测。我们没有观察到那 8 个站出任何问题,本篇只记录声明、不记录缓存行为;而现代 CDN 普遍把压缩当特例处理,不完全依赖这一行。这大概也是它能一直没事的原因,但「大概」两个字要保留——具体到某一家 CDN 的某一档配置,得自己抓包确认。

出海站要不要加 Vary: User-Agent

只有一种情况要加:同一个网址按设备返回不同的 HTML。Google 的移动优先文档把这个字段绑在同址分流上,并建议改用响应式设计。本次 27 个站没有一个发它。

为什么有 4 个站只列了框架的路由字段

不知道。一次请求分不开这几种可能:源站发的头被下游覆盖了、边缘把它剥掉重写了、或者压缩发生在压根不看这一行的那一层。四个站跑的是同一个框架、从外面看长得一模一样,这是一个规律,不是一个解释。

vary header 实测 27 个首页:27 个全在压缩,8 个从没声明过