了解 Vary 标头
已发表: 2022-03-10Vary HTTP 标头每天以数十亿的 HTTP 响应发送。 但它的使用从未实现其最初的愿景,许多开发人员误解了它的作用,甚至没有意识到他们的 Web 服务器正在发送它。 随着客户端提示、变体和关键规范的出现,各种响应正在重新开始。
什么是变化?
Vary 的故事始于一个关于网络应该如何工作的美好想法。 原则上,URL 代表的不是网页,而是概念资源,例如您的银行对帐单。 想象一下,您想查看您的银行对帐单:您访问bank.com
并为/statement
发送一个GET
请求。 到目前为止一切都很好,但是您没有说明您想要声明的格式。这就是为什么您的浏览器还会在您的请求中包含诸如Accept: text/html
之类的内容。 至少在理论上,这意味着您可以说Accept: text/csv
并以不同的格式获取相同的资源。

因为同一个 URL 现在会根据Accept
标头的值产生不同的响应,所以任何存储此响应的缓存都需要知道该标头很重要。 服务器告诉我们Accept
标头很重要,如下所示:
Vary: Accept
您可以将其解读为“此响应因您的请求的Accept
标头的值而异。”
这在今天的网络上基本上行不通。 所谓的“内容协商”是个好主意,但失败了。 但这并不意味着Vary
没有用。 您在网络上访问的页面的相当一部分在响应中带有Vary
标头 - 也许您的网站也有它们,而您不知道。 那么,如果 header 不能用于内容协商,为什么它仍然如此流行,浏览器又是如何处理的呢? 让我们来看看。
我之前写过关于内容交付网络 (CDN) 的 Vary,这些中间缓存(例如 Fastly、CloudFront 和 Akamai)可以放在服务器和用户之间。 浏览器也需要理解和响应 Vary 规则,而他们这样做的方式与 CDN 处理 Vary 的方式不同。 在这篇文章中,我将探索浏览器中缓存变化的阴暗世界。
当今在浏览器中变化的用例
正如我们之前看到的,Vary 的传统用途是使用Accept
、 Accept-Language
和Accept-Encoding
标头执行内容协商,而从历史上看,其中前两个已惨遭失败。 改变Accept-Encoding
以提供 Gzip 或 Brotli 压缩的响应,在受支持的情况下,大多数情况下工作得相当好,但是现在所有的浏览器都支持 Gzip,所以这不是很令人兴奋。
其中一些场景怎么样?
- 我们希望提供与用户屏幕宽度完全一致的图像。 如果用户调整他们的浏览器大小,我们将下载新图像(根据客户端提示而有所不同)。
- 如果用户注销,我们希望避免使用他们登录时缓存的任何页面(使用 cookie 作为
Key
)。 - 支持 WebP 图片格式的浏览器用户应该会得到 WebP 图片; 否则,他们应该得到 JPEG。
- 在高密度屏幕上使用浏览器时,用户应该获得 2x 图像。 如果他们将浏览器窗口移动到标准密度屏幕上并刷新,他们应该得到 1x 图像。
一路缓存
与充当所有用户共享的巨大缓存的边缘缓存不同,浏览器仅适用于一个用户,但它具有许多不同的缓存用于不同的特定用途:

其中一些是相当新的,并且准确了解正在从哪个缓存加载内容是一个复杂的计算,开发人员工具不能很好地支持。 以下是这些缓存的作用:
- 图像缓存
这是一个页面范围的缓存,用于存储解码后的图像数据,因此,例如,如果您在一个页面上多次包含相同的图像,浏览器只需要下载和解码一次。 - 预加载缓存
这也是页面范围的,并存储任何已预加载在Link
标头或<link rel="preload">
标记中的内容,即使该资源通常是不可缓存的。 与图像缓存一样,当用户离开页面时,预加载缓存会被破坏。 - 服务工作者缓存 API
这提供了一个带有可编程接口的缓存后端; 因此,除非您通过服务工作者中的 JavaScript 代码专门将其放置在此处,否则此处不会存储任何内容。 如果您在服务工作者fetch
处理程序中明确这样做,它也只会被检查。 Service Worker 缓存是原始范围的,虽然不能保证是持久的,但它比浏览器的 HTTP 缓存更持久。 - HTTP 缓存
这是人们最熟悉的主要缓存。 它是唯一关注 HTTP 级缓存头(如Cache-Control
)的缓存,并将这些与浏览器自己的启发式规则相结合,以确定是否缓存某些内容以及缓存多长时间。 它的范围最广,被所有网站共享; 因此,如果两个不相关的网站加载相同的资产(例如 Google Analytics),它们可能会共享相同的缓存命中。 - HTTP/2 推送缓存(或“H2 推送缓存”)
这与连接一起存在,它存储已从服务器推送但尚未被使用该连接的任何页面请求的对象。 它的作用域是使用特定连接的页面,这与作用于单个源的作用基本相同,但是当连接关闭时它也会被销毁。
其中,HTTP 缓存和 Service Worker 缓存是最好定义的。 至于图像和预加载缓存,一些浏览器可能会将它们实现为与特定导航的渲染相关联的单个“内存缓存”,但我在这里描述的心理模型仍然是思考该过程的正确方法。 如果您有兴趣,请参阅有关preload
的规范说明。 在 H2 服务器推送的情况下,关于这个缓存的命运的讨论仍然活跃。
请求在进入网络之前检查这些缓存的顺序很重要,因为请求某些内容可能会将其从缓存的外层拉到内部缓存。 例如,如果您的 HTTP/2 服务器将样式表与需要它的页面一起推送,并且该页面还使用<link rel="preload">
标记预加载样式表,那么样式表最终将触及三个缓存在浏览器中。 首先,它将位于 H2 推送缓存中,等待被请求。 当浏览器渲染页面并到达preload
标签时,它将通过 HTTP 缓存(可能会存储它,取决于样式表的Cache-Control
标头)将样式表从推送缓存中拉出,并将保存它在预加载缓存中。

介绍 Vary 作为验证者
好的,那么当我们在这种情况下添加 Vary 时会发生什么?
与中间缓存(例如 CDN)不同,浏览器通常不会实现为每个 URL 存储多个变体的功能。 这样做的理由是我们通常使用Vary
的东西(主要是Accept-Encoding
和Accept-Language
)在单个用户的上下文中不会经常更改。 Accept-Encoding
可能(但可能不会)在浏览器升级时发生变化,而Accept-Language
很可能只有在您编辑操作系统的语言区域设置时才会发生变化。 以这种方式实现 Vary 也更容易,尽管一些规范作者认为这是一个错误。
大多数情况下,浏览器只存储一个变体并不是什么大损失,但重要的是,如果“varied on”数据确实发生了变化,我们不会意外地使用不再有效的变体。
折衷方案是将Vary
视为验证者,而不是密钥。 浏览器以正常方式计算缓存键(本质上是使用 URL),然后如果它们获得命中,它们会检查请求是否满足缓存响应中的任何 Vary 规则。 如果不是,则浏览器将请求视为缓存未命中,并继续移动到下一层缓存或移出网络。 当收到新的响应时,它将覆盖缓存的版本,即使它在技术上是不同的变体。

展示不同的行为
为了演示Vary
的处理方式,我制作了一个小测试套件。 该测试加载一系列不同的 URL,在不同的标头上有所不同,并检测请求是否已命中缓存。 我最初为此使用 ResourceTiming,但为了更好的兼容性,我最终切换到仅测量请求完成所需的时间(并故意为服务器端响应添加 1 秒延迟以使差异非常明显)。
让我们看看每种缓存类型以及Vary
应该如何工作以及它是否真的像那样工作。 对于每个测试,我在这里展示我们是否应该期望从缓存中看到结果(“HIT”与“MISS”)以及实际发生的情况。
预载
目前仅在 Chrome 中支持预加载,其中预加载的响应存储在内存缓存中,直到页面需要它们。 如果响应是 HTTP 可缓存的,则响应还会在到达预加载缓存的途中填充 HTTP 缓存。 因为指定带有预加载的请求标头是不可能的,并且预加载缓存只持续与页面一样长,因此测试这很困难,但我们至少可以看到带有Vary
标头的对象确实被成功预加载:

服务工作者缓存 API
Chrome 和 Firefox 支持 Service Worker,在开发 Service Worker 规范时,作者希望修复他们认为浏览器中损坏的实现,使浏览器中的Vary
更像 CDN。 这意味着虽然浏览器应该只在 HTTP 缓存中存储一个变体,但它应该在 Cache API 中保存多个变体。 Firefox (54) 正确地做到了这一点,而 Chrome 使用与用于 HTTP 缓存的相同的可变验证器逻辑(正在跟踪该错误)。

HTTP 缓存
主 HTTP 缓存应遵守Vary
并在所有浏览器中始终如一地这样做(作为验证器)。 有关这方面的更多信息,请参阅 Mark Nottingham 的文章“浏览器缓存状态,再访”。
HTTP/2 推送缓存
应该观察到Vary
,但实际上没有浏览器真正尊重它,浏览器会很高兴地匹配和消费推送的响应与响应不同的标头中带有随机值的请求。

“304(未修改)”皱纹
HTTP“304(未修改)”响应状态令人着迷。 我们的“亲爱的领导者”阿图尔·伯格曼(Artur Bergman)向我指出了 HTTP 缓存规范中的这个宝石(强调我的):
生成 304 响应的服务器必须生成以下头字段中的任何一个,这些头字段将在对同一请求的 200(OK)响应中发送:
Cache-Control
、Content-Location
、Date
、ETag
、Expires
和Vary
。
为什么304
响应会返回Vary
标头? 当您阅读到收到包含这些标头的304
响应时您应该做什么时,情节会变厚:
如果选择存储响应进行更新,则缓存必须\[...] 使用 304(未修改)响应中提供的其他头字段来替换存储响应中相应头字段的所有实例。
等等,什么? 那么,如果304
的Vary
标头与现有缓存对象中的不同,我们应该更新缓存对象吗? 但这可能意味着它不再符合我们提出的要求!
在这种情况下,乍一看, 304
似乎同时告诉您可以使用缓存版本,也不能使用缓存版本。 当然,如果服务器真的不想让你使用缓存版本,它会发送200
,而不是304
; 因此,绝对应该使用缓存版本——但在对其应用更新后,它可能不会再次用于与最初实际填充缓存的请求相同的未来请求。
(旁注:在 Fastly,我们不尊重规范的这种怪癖。因此,如果我们从您的源服务器收到304
,我们将继续使用未修改的缓存对象,而不是重置 TTL。)
浏览器似乎确实尊重这一点,但有一个怪癖。 它们不仅更新响应标头,还更新与之配对的请求标头,以保证更新后缓存的响应与当前请求匹配。 这似乎是有道理的。 规范没有提到这一点,因此浏览器供应商可以自由地做他们喜欢的事情; 幸运的是,所有浏览器都表现出相同的行为。
客户提示
Google 的客户端提示功能是 Vary 长期以来在浏览器中发生的最重要的新功能之一。 与Accept-Encoding
和Accept-Language
不同,客户端提示描述的值可能会随着用户在您的网站上移动而定期更改,具体如下:
-
DPR
设备像素比,屏幕的像素密度(如果用户有多个屏幕,可能会有所不同) -
Save-Data
用户是否开启了节流模式 Viewport-Width
当前视口的像素宽度Width
以物理像素为单位的所需资源宽度
不仅对于单个用户,这些值可能会发生变化,而且与宽度相关的值的范围很大。 因此,我们完全可以对这些标头使用Vary
,但我们冒着降低缓存效率甚至使缓存无效的风险。
关键标题提案
客户端提示和其他高度细化的标头适用于 Mark 一直在研究的名为 Key 的提案。 让我们看几个例子:
Key: Viewport-Width;div=50
这表示响应会根据Viewport-Width
请求标头的值而变化,但会向下舍入到最接近的 50 像素的倍数!
Key: cookie;param=sessionAuth;param=flags
将此标头添加到响应中意味着我们在两个特定的 cookie 上有所不同: sessionAuth
和flags
。 如果它们没有改变,我们可以将此响应重用于未来的请求。
因此, Key
和Vary
之间的主要区别是:
-
Key
允许对标头中的子字段进行更改,这突然使更改 cookie 变得可行,因为您只能更改一个 cookie — 这将是巨大的; - 可以将单个值存储到范围中,以增加缓存命中的机会,这对于改变诸如视口宽度之类的东西特别有用。
- 具有相同 URL 的所有变体必须具有相同的键。 因此,如果缓存接收到一个新响应的 URL 已经有一些现有变体,并且新响应的
Key
标头值与那些现有变体上的值不匹配,则必须从缓存中逐出所有变体。
在撰写本文时,没有浏览器或 CDN 支持Key
,尽管在某些 CDN 中,您可以通过将传入的标头拆分为多个私有标头并对其进行更改来获得相同的效果(请参阅我们的帖子,“Getting the Most Out of Vary With Fastly”),因此浏览器是Key
可以产生影响的主要领域。
所有变体都具有相同的关键配方的要求有些限制,我希望在规范中看到某种“提前退出”选项。 这将使您能够执行诸如“改变身份验证状态,如果登录,也改变偏好”之类的事情。
变体提案
Key
是一个很好的通用机制,但是一些头部的值有更复杂的规则,理解这些值的语义可以帮助我们找到减少缓存变化的自动化方法。 例如,假设有两个请求带有不同Accept-Language
值en-gb
和en-us
,但是尽管您的网站确实支持语言变化,但您只有一个“英语”。 如果我们回答美国英语的请求并且该响应被缓存在 CDN 上,那么它不能被重用于英国英语请求,因为Accept-Language
值会不同并且缓存不够聪明,无法更好地了解.
大张旗鼓地输入变体提案。 这将使服务器能够描述它们支持的变体,允许缓存做出更明智的决定,即哪些变体实际上是不同的,哪些实际上是相同的。
目前,Variants 是一个非常早期的草案,因为它旨在帮助Accept-Encoding
和Accept-Language
,它的用处仅限于共享缓存,例如 CDN,而不是浏览器缓存。 但它很好地与Key
配对并完成了图片以更好地控制缓存变化。
结论
这里有很多东西可以吸收,虽然了解浏览器如何在后台工作可能很有趣,但您也可以从中提取一些简单的东西:
- 大多数浏览器将
Vary
视为验证器。 如果您希望缓存多个单独的变体,请找到一种使用不同 URL 的方法。 - 对于使用 HTTP/2 服务器推送推送的资源,浏览器会忽略
Vary
,因此不要对推送的任何内容进行更改。 - 浏览器有大量的缓存,它们以不同的方式工作。 值得尝试了解您的缓存决策如何影响每一项的性能,尤其是在
Vary
的上下文中。 -
Vary
并没有它应有的用处,而Key
与 Client Hints 配对开始改变这一点。 跟随浏览器支持了解何时可以开始使用它们。
继续前进,多变。