HTTP/2 push 比我想象的要难

HTTP/2 Push 比我想象的要难一些

关于页面加载性能问题,我经常听到 “这个用 HTTP/2 Push 可以解决啊” 这样的话,但是我对于 “HTTP/2 Push” 并不了解,所以我决定深入研究一下并写下这篇文章。

HTTP/2 Push 比我最初想象的要复杂而且要低级一些,但真正让我措手不及的是浏览器之间对于 HTTPS/2 Push 兼容的的不一致,即使我认为这已经完成了我的工作并且已经完全可以投入生产。

这不是对 HTTP/2 Push 这样的的恶意诽谤,我认为 HTTP/2 Push 非常强大,并且会随着时间的推移而逐渐改进,但我再也不认为这是金枪银弹了。

图片示意

在您的页面和目标服务器之间有一系列缓存,可以拦截请求:
在这里插入图片描述

上面的数据流图可能与人们用来解释 Gitobservables 的流程图很相似,你过你熟悉这种图那么你可能很容易看懂上图,如果你没有看过类似的图也不要惊慌,希望接下来的几节能有所帮助。

HTTP/2 push 如何工作?

在这里插入图片描述

当服务器响应请求时,它可以包含额外的一些资源,其中包括一组请求头,因此浏览器可以知道如何处理它。在浏览器请求与其描述相匹配的资源之前,它们将一直保存在缓存中。

HTTP/2 Push 之所以能够提高性能,是因你无需等待浏览器要求就开始发送资源。从理论上讲,这意味着页面加载速度将会更快。

这些年来,我几乎对 HTTP/2 Push 一无所知,这个技术听起来挺简单,但研究到细节就发现其可怕之处……

任何人都可以使用 Push Cache

HTTP/2 Push是一个低级的网络特性,任何使用网络协议栈的东西都可以利用它。它最关键的特性是一致性和可预测性。

我通过推动资源并尝试通过以下方式来收集资源:

  • fetch()
  • XMLHttpRequest
  • <link rel="stylesheet" href="…">
  • <script src="…">
  • <iframe src="…">

我还放慢了推送资源 body 的交付,以查看浏览器是否与仍在推送的项目匹配。 在我的 github 上你可以下载一个很简单的测试套件。

Edge: 使用fetch()XMLHttpRequest<iframe> ([问题和视频](https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12142852/)时,Edge不会从推送缓存中检索项目。

Safari: Safari很奇怪。 什么时候使用/不使用 push cache,似乎就像掷硬币一样。 Safari遵循OSX的开源网络堆栈,但我认为Safari可能存在一些错误。 似乎它打开了太多的连接,并且 push 的项目最终在它们之间分配。 这意味着,只有在请求足够幸运可以使用相同连接的情况下,你才能获得缓存命中,但这确实超出了我的脑力(问题视频)。

所有浏览器(除了奇怪的Safari)即使仍在推送中的时候,也都会使用匹配的推送项目,,这点我认为很好。

不幸的是,Chrome 是唯一支持 devtools 的浏览器。 网络面板将告诉您已从推送缓存中提取了哪些项目。

建议:

如果浏览器无法从推送缓存中检索项目,则最终结果将比根本没有推送的情况要慢。

Edge的支持一直以来都很差。 你可以使用用户代理嗅探来确保只 push 你需要使用资源。 如果由于某种原因无法实现,那么避免向Edge用户推送任何内容可能会更安全。

Safari的行为总是充满不确定,所以你可能无处下手。 可以使用用户代理嗅探来避免将资源推送给Safari用户。

你可以 push no-cache 和 no-store resources

使用HTTP缓存时,项目必须具有类似 max-age 的属性来允许浏览器在不重新验证服务器的情况下使用缓存(这是有关缓存头的文章)。 HTTP/2 Push 和匹配项目的时候不检查“新鲜度”的机制是不一样的。

所有的浏览器都支持这种方式
所有的浏览器都支持
建议:
一些单页应用程序的性能会受到影响,因为它们不仅会被JS阻止渲染,还会被某些数据(JSON或其他)阻止,一旦JS执行,它们就会开始获取。 服务器渲染可能是最好的的解决方案,但是如果不可能进行服务器渲染的话,则可以将JS和JSON随页面一起推送。

但是,鉴于前面提到的Edge / Safari问题,内联JSON更可靠。

HTTP/2 Push 缓存是浏览器检查的最后一个缓存

推送项目带有 HTTP/2 连接,这意味着浏览器在提供响应之前只会在没有任何内容的情况下使用推送项目。 包括图像缓存,预加载缓存,service worker 和HTTP缓存。

所有的浏览器都支持这种方式
所有的浏览器都支持
建议:
请注意。 例如,如果你在HTTP缓存中有一个匹配项根据其 max-age 是最新的,并且您push了一个较新的项目,则push的项目将被忽略,而使用HTTP缓存中的旧项(除非API 无论出于何种原因绕过HTTP缓存)。

在链中排在最后并不是一个真正的问题,但是了解缓存项与连接之间的关系有助于我了解很多其他行为。 例如…

如果连接关闭,向 push cache 说拜拜

push 缓存位于HTTP/2连接上,因此,如果连接关闭,则会丢失缓存。 即使推送的资源具有高度可缓存性,也会发生这种情况。

push 缓存位于HTTP缓存之外,因此,只有在浏览器请求它们之后,项目才进入HTTP缓存。 那时,它们已通过HTTP缓存,service worker 等从推送缓存中拉出,并进入了页面。

如果用户的连接不稳定,则可以成功 push 某些内容,但是在页面设法获得连接之前就失去了连接。 这意味着他们将必须建立新的连接并重新下载资源。

所有的浏览器都支持这种方式
所有的浏览器都支持
建议:
不要长时间依赖 push 缓存中没用的东西。 push 最适合用于紧急资源,因此在推送资源与页面拾取之间应该没有多少时间。

多个页面可以使用同一个的 HTTP/2 连接

每个连接都有自己的 push 缓存,但是多个页面可以使用一个连接,这意味着多个页面可以共享一个 push 缓存。

实际上,这意味着如果您将资源与导航响应(例如HTML页面)一起 push,则该资源并非仅可用于该页面(在本文的其余部分中,我将使用“pages”代替, 默认包括可以获取资源的其他上下文,比如 workers)。

Edge似乎为每个 tab 页面(问题视频)都使用了新的连接。

Safari不必要地创建到相同来源的多个连接。(问题视频)。

建议:
当你将JSON数据之类的内容与页面一起 push 时,请注意!你不能依赖同一页面来提取它。

这种行为可能会成为一种优势,因为你与页面一起 push 的资源可以被安装服务工作程序发出的请求提取。

Edge的行为并不是最好的,但是现在不需要担心。 一旦Edge获得service worker的支持,就可能成为问题。

同样,我将避免为Safari用户 push 资源。

没有凭据的请求使用单独的连接

本文你将会看到多次“Credentials”。 Credentials(凭证)是浏览器发送的用于标识特定用户的东西。 这通常表示cookie,但也可能表示HTTP基本身份验证和连接级别标识符,例如客户端证书。

如果您将 HTTP/2 连接想象成打电话☎,在你介绍自己之后,你就不再是匿名的了,并且包括你在介绍自己之前所说的一切。 出于隐私原因,浏览器为“匿名”请求设置了单独的“呼叫”。

但是,由于 push 缓存位于连接中,因此您可以通过发出非凭据请求来最终丢失丢失的缓存项。 例如,如果您将资源与页面(Credentials请求)一起推送,然后对它进行 fetch() (非non-credentialed),它将建立一个新的连接,并且错过了 push 的项目。

如果 cross-origin stylesheet (Credentials)推送字体,则浏览器的字体请求(non-credentialed)将丢失 push 缓存中的字体。
建议:
确保您的请求使用相同的Credentials模式。 在大多数情况下,这意味着确保您的请求包括Credentials,因为页面请求始终使用Credentials进行。

要获取Credentials,请使用:

fetch(url, { credentials: 'include' });

您不能将Credentials添加到跨域字体请求,但可以将其从样式表中删除:
<link rel="stylesheet" href="…" crossorigin />
…这意味着样式表和字体请求将处于同一连接状态。 但是,如果该样式表也应用了背景图像,则这些请求将始终具有凭据,因此您将再次获得另一个连接。 唯一的解决方案是 service worker,它可以更改每个请求的获取方式。

我听开发人员说,非凭据请求不需要发送Cookie,因此它们的性能更好,但是你必须权衡这与建立新连接的更高成本。 同样,HTTP/2 可以压缩请求之间重复的标头,因此cookie并不是真正的问题。

也许我们应该改变规则:
Edge是唯一不遵循此处规则的浏览器。 它允许凭据和非凭据请求共享连接。 但是,我跳过了通常的浏览器图标行,因为我希望看到此处的规格发生更改。

如果页面对其源发出非凭据请求,则建立单独的连接毫无意义。 凭证资源启动了该请求,因此可以通过URL将其凭证添加到“匿名”请求中。

对于其他情况,我不太确定,但是由于浏览器指纹识别,如果您要向同一服务器发出凭据和非凭据请求,那么匿名的方式就很少。 如果您想对此进行更深入的了解,可以在GitHubMozillaFirefox’s bug tracker上进行讨论。

push cache 中的项目只能使用一次

一旦浏览器在推送缓存中使用了某些内容,便将其删除。 它可能最终出现在HTTP缓存中(取决于缓存头),但不再存在于推送缓存中。

Safari在这里有很多的困扰。 如果在推送时多次提取资源,它将多次获取推送的项目(问题视频)。 如果在项目完成推送后获取了两次,它将正常工作,第一次将从推送缓存中返回,而第二次则不会。
建议:
如果你决定将内容推送给Safari用户,则在推送无缓存资源(例如JSON数据)时请注意此错误。 也许将随机ID随响应一起传递,并且如果两次返回相同的ID,则说明您已经遇到了错误。 在这种情况下,请稍等片刻,然后重试。

通常,一旦获取推送的资源,就使用缓存头或 service worker 来缓存它们,除非不需要缓存(例如一次性JSON提取)。

浏览器可以中止已推送的项目

当你推送内容时,无需与客户端进行太多协商即可完成。 这意味着你可以推送浏览器已在其缓存之一中包含的内容。 在这种情况下,HTTP/2 规范允许浏览器使用 CANCELREFUSED_STREAM 代码中止传入流,以避免浪费带宽。

这个的规范并不严格,所以我在这里的判断是基于对开发人员有用的。
Chrome如果缓存中已经包含该项目,则Chrome将拒绝该推送。它使用 PROTOCOL_ERROR 而不是 CANCELREFUSED_STREAM 拒绝,但这是一件小事(问题)。不幸的是,它不会拒绝HTTP缓存中已有的项目。听起来这几乎是固定的,但我无法对其进行测试(问题)。

Safari 如果缓存中已经包含该项目,则Safari将拒绝该推送,但前提是根据缓存标头(例如max-age),除非该推送缓存中的项目为“新鲜”,除非用户点击刷新。这与Chrome不同,但我认为这不是“错误”。不幸的是,像Chrome一样,它不会拒绝HTTP缓存(问题)中已经存在的项目。

Firefox 如果缓存中已经包含该项目,Firefox将拒绝推,但随后它也会删除它在推缓存中已经存在的项目,因此一无所有!这使得它非常不可靠,并且很难防御(问题视频)。 Firefox也不会拒绝HTTP缓存(问题)中已经存在的项目。

Edge 不会拒绝 push 缓存中已有项目的推送,但会拒绝该项目是否位于HTTP缓存中。

建议:
不幸的是,即使有了完善的浏览器支持,您在获得取消消息之前也会浪费带宽和服务器 I/O 。 缓存摘要旨在通过告诉服务器提前缓存的内容来解决此问题。

同时,你可能希望使用cookie来跟踪是否已将可缓存资源推给用户。 但是,浏览器一时兴起,项目可能会从HTTP缓存中消失,而cookie仍然存在,因此cookie的存在并不意味着用户仍然在其缓存中保留这些项目。

除了更新外,push cache 中的项目还应使用HTTP语义进行匹配

我们已经看到,在 push 缓存中匹配项目时,新鲜度(freshness)被忽略了(这就是 no-storeno-cache 项目的匹配方式),但是应该使用其他匹配机制。 我测试了 POS T请求,以及 Vary: Cookie

**更新:**规范说推送的请求“必须是可缓存的,必须是安全的,并且不得包含请求主体” –我最初错过了这些定义。 POST 请求不属于“安全”的定义,因此浏览器应拒绝 POST

Chrome 接受 push 的 POST,但似乎没有使用它们。 当匹配推送的项目(问题)时,Chrome也忽略了Vary标头,尽管该问题表明它在使用QUIC时有效。

Firefox 拒绝 push 的 POST 。 但是,当匹配推送的项目(问题时,Firefox会忽略Vary标头。

Edge 拒绝 push 的 POST 。 但也忽略了Vary头(问题)。

Safari 像Chrome一样,Safari也接受 push 的 POST ,但似乎没有使用它们。 它确实服从Vary标头,并且它是唯一这样做的浏览器。

建议:
除了Safari其他浏览器都没有注意Vary头中的已推送项目。 这意味着你可以推送一些供一个用户使用的JSON,然后该用户注销而另一用户登录,但是如果尚未收集前一个用户的JSON,则仍然可以得到它。

如果你要推送给一个用户的数据,请同时提供预期的用户ID。 如果与你期望的有所不同,可以再次发出请求(因为推送的项目会消失)。

在Chrome浏览器中,您可以在用户注销时使用清除站点数据头。 这也通过终止HTTP/2连接来清除 push 缓存中的项目。

你可以向其他源 push 项目

作为 developers.google.com/web 的开发者,我们可以让我们的服务器推送包含我们想要的 android.com 内容的响应,并将其设置为缓存一年。 简单的提取就足以将其拖动到HTTP缓存中。 然后,如果我们的用户访问了 android.com,他们会看到大大的红色“NATIVE SUX – PWA RULEZ”标记,或我们想要的任何东西。

当然,我们不会那样做,我们喜欢 Android 。 我只是在说……Android:如果您搞砸了网络,我们将为您服务。

好吧,我开玩笑,但是上面的方法确实有效。 您不能将缓存推入任何源,但是可以将它们推入你的连接对其具有“权威性”的来源。

如果您查看 developer.google.com 的证书,可以看到该证书对所有Google源(包括android.com)都是权威的。

【此处链接挂了】

OK,我撒谎了,因为当我们获取android.com时,它将执行DNS查找,并发现它以不同于 developers.google.com 的IP终止,因此它将建立新的连接并丢失我们的项目在 push 缓存中。

我们可以使用ORIGIN框架解决此问题。 这使连接说“嘿,如果您需要android.com上的任何东西,请问我。不需要做任何DNS东西”,只要它是权威性的即可。 这对于常规连接合并很有用,但它是一个很新的功能,仅在Firefox Nightly中受支持。

如果你使用的是CDN或某种共享主机,请查看证书,查看哪些来源可以开始为你的网站推送内容。 有点恐怖 值得庆幸的是,没有一个主机(我知道)可以完全控制 HTTP/2 push,并且不太可能要感谢规范中的这一小注释:

Where multiple tenants share space on the same server, that server MUST ensure that tenants are not able to push representations of resources that they do not have authority over.
— HTTP/2 spec

Chrome 允许网站为它拥有权限的源 push 资源。 如果另一个源终止于相同的IP,它将重用连接,因此将使用那些推送的项目。 Chrome浏览器尚不支持ORIGIN框架。

Safari 允许网站为它拥有权限的源 push 资源,但是它为其他源建立了新的连接,因此这些推送项从未使用过。 Safari不支持ORIGIN框架。

Firefox 拒绝其他源的推送。 像Safari一样,它为其他来源建立了新的连接。 但是,我绕过了Firefox中的证书警告,因此我对结果不满意。 Firefox Nightly支持ORIGIN框架。

Edge 拒绝其他来源的推送。 同样,我绕过了证书警告,因此,使用适当的证书,这些结果可能会有所不同。 Edge不支持ORIGIN框架。
建议:
如果你使用同一页面上的多个源,而这些源最终使用同一服务器,则开始查看ORIGIN框架。 一旦得到支持,它就无需进行DNS查找,从而提高了性能。

如果你认为可以跨域 push 更好的话,请编写比我更好的测试用例,并确保浏览器将实际使用你 push 的内容。 否则,使用用户代理嗅探来推送到特定的浏览器。

推送与预加载

你可以要求浏览器使用HTML预先加载资源,而不是 push 资源:

<link
  rel="preload"
  href="https://fonts.example.com/font.woff2"
  as="font"
  crossorigin
  type="font/woff2"
/>

或头:

Link: <https://fonts.example.com/font.woff2>; rel=preload; as=font; crossorigin; type='font/woff2'
  • href 要预加载的URL
  • as 响应的目的地。 这意味着浏览器可以设置正确的标题并应用正确的CSP策略。
  • crossorigin 可选。 指示该请求应为CORS请求。 除非crossorigin =“ use-credentials”,否则将不使用凭据发送CORS请求。
  • type 可选。 如果不支持提供的MIME类型,则允许浏览器忽略预加载。

浏览器看到预加载链接后,便会提取该链接。 该功能类似于 HTTP/2 push,因为:

  • 任何东西都可以加载。
  • 可以预加载no-cacheno-store
  • 如果请求的凭据模式相同,则你的请求将仅与预加载的项匹配。
  • 缓存的项目只能使用一次,尽管它们可能位于HTTP缓存中以供将来提取。
  • 除了新鲜度之外,还应使用HTTP语义对项目进行匹配。
  • 你可以预加载其他来源的项目。

不同之处:
浏览器获取资源,这意味着它将按该顺序查找service worker,HTTP缓存,HTTP/2缓存或目标服务器的响应。
预加载的资源与页面(或worker)一起存储。 这使其成为浏览器将首先检查的缓存之一(在service worker和HTTP缓存之前),并且失去连接并不会失去你预加载的项目。 意味着页面还直接链接,如果不使用预加载的项目,devtools可以显示有用的警告。

每个页面都有自己的预加载缓存,因此预加载打算用于另一个页面的内容是没有意义的。 与之类似,您不能在页面加载后预加载要使用的项目。 从页面中预加载内容以用于Service Worker安装也是没有意义的,Service Worker不会检查页面的预加载缓存。

Chrome不支持所有API的预加载。 例如,fetch()不使用预加载缓存。 XHR 可以,但仅当它与凭据一起发送时才可以。(问题

Safari 仅在其最新技术预览中支持预加载。 fetch() 不使用预加载缓存,XHR也不可以。(问题

Firefox 不支持预加载,但是它们的实现正在进行中(问题)。

Edge 不支持预加载。 如果需要,请给它点赞。

建议:
完美的预加载总是比完美的 HTTP/2 push 慢一点,因为它不需要等待浏览器发出请求。 但是,预加载大大简化了并且易于调试。 我建议你现在使用它,因为浏览器支持只会越来越好,但是请务必注意devtools以确保使用你的推送项。

某些服务会将预加载标头转换为HTTP / 2推送。 考虑到两者之间的行为有多么细微的差别,我认为这是一个错误,但这可能是我们将不得不忍受一段时间的事情。 但是,您应该确保这些服务从最终响应中删除标头,否则可能会遇到竞态条件,即在推送之前发生预加载,从而导致带宽使用量增加一倍。

push 的未来

目前,围绕 HTTP/2 push 存在一些非常老的错误,但是一旦修复这些错误,我认为它对于我们当前内联的各种资源(尤其是渲染关键型CSS)来说将是理想的。

要实现这个目标,将取决于更智能的服务器,这些服务器可使我们正确地确定内容流的优先级。例如,我希望能够与页面的头部并行地传输关键的CSS,但随后将CSS完全优先处理,因为这浪费了用户尚无法呈现的正文内容的带宽。

如果你的服务器响应速度有些慢(由于昂贵的数据库查找或其他原因),则你也可以用这段时间来推送页面可能需要的资产,然后在页面可用时更改优先级。

就像我说的那样,这篇文章不是一件容易的事,我希望它不会成为一体。 HTTP/2 push 可以提高性能,只是在没有仔细测试的情况下不要使用它,否则可能会使速度变慢。

感谢Gray NortonAddy OsmaniSurma进行校对并提供反馈,以及Mencre的翻译工作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值