HTTP/2 服务端推送全面指南

大约 19 分钟networking网络网络HTTPHTTP/2server-push

HTTP/2 服务端推送全面指南

对于注重性能的开发者来说,过去一年左右的时间里,性能优化的领域发生了很大的变化,其中 HTTP/2 的出现可能是最重要的。HTTP/2 不再是我们渴望拥有的特性,它已经到来了,而且还带来了服务器推送!除了解决常见的 HTTP/1 性能问题(例如头部阻塞和未压缩的头部),HTTP/2 还为我们提供了服务器推送!服务器推送可以让您在用户请求之前发送站点资源。这是一种优雅的方式,可以实现 HTTP/1 优化实践(例如内联),但没有该实践带来的缺点。

在本文中,您将学习有关服务器推送的全部知识,从它是如何工作的到它解决的问题。您还将了解如何使用它,如何确定它是否起作用以及它对性能的影响。让我们开始吧!

什么是服务器推送?

访问网站始终遵循请求和响应模式。用户发送请求到远程服务器,服务器稍有延迟后响应所请求的内容。

对 Web 服务器的初始请求通常是针对 HTML 文档的。在这种情况下,服务器将以请求的 HTML 资源进行响应。然后,浏览器解析 HTML,在其中发现对其他资源的引用,例如样式表、脚本和图像。发现这些引用后,浏览器将单独请求这些资源,然后分别进行响应。

这种机制的问题在于它强制用户等待浏览器在下载完HTML文档之后才发现和检索关键资源。这会延迟渲染并增加加载时间。

使用服务器推送,我们可以解决这个问题。服务器推送允许服务器主动向客户端“推送”网站资源,而无需用户显式请求。如果小心使用,我们可以发送我们知道用户要请求页面所需的内容。

假设您有一个网站,所有页面都依赖于一个名为styles.css的外部样式表中定义的样式。当用户从服务器请求index.html时,我们可以在开始发送index.html的响应后立即向用户推送styles.css。

与其等待服务器发送index.html然后等待浏览器请求并接收styles.css,用户只需等待服务器在初始请求中回复index.htmlstyles.css两个文件。这意味着浏览器可以比等待更快地开始渲染页面。

可以想象,这可以减少页面的渲染时间。它还解决了一些其他问题,特别是在前端开发工作流程中。

服务器推送解决了哪些问题?

虽然减少关键内容要向服务器的往返是服务器推送解决的问题之一,但这并不是唯一的问题。服务器推送作为HTTP/1特定优化反模式的适当替代方案,例如将CSS和JavaScript直接内联到HTML中,以及使用data URI方案open in new window将二进制数据嵌入CSS和HTML中。

这些技术在HTTP/1优化工作流程中得到应用,因为它们减少了我们称之为页面“感知渲染时间”,这意味着虽然页面的整体加载时间可能没有减少,但页面对于用户来说会显示得更快。这很有道理。如果您在HTML文档中内联CSS,那么浏览器可以立即开始将样式应用于HTML而无需等待从外部源获取它们。这个概念也适用于内联脚本和内联二进制数据使用data URI方案。

看起来这是解决问题的好方法,对吗?确实——对于HTTP/1工作流程而言,你别无选择。然而,当我们这样做时,我们吞下的毒丸是内联内容无法有效地缓存。当像样式表或JavaScript文件这样的资源保持外部和模块化时,它可以被更有效地缓存。当用户导航到需要该资源的后续页面时,它可以从缓存中提取,消除了对服务器发出其他请求的需要。

当我们内联内容时,该内容没有自己的缓存上下文。它的缓存上下文与嵌入它的资源相同。例如,考虑一个内联CSS的HTML文档。如果HTML文档的缓存策略始终从服务器获取标记的新副本,那么内联CSS将永远不会被单独缓存。当然,它所属的文档可能被缓存,但包含此重复的CSS的后续页面将被重复下载。即使缓存策略更为宽松,HTML文档通常也有有限的寿命。尽管如此,这是我们在HTTP/1优化工作流程中愿意做出的权衡。它确实有效,对于首次访问者来说非常有效。第一印象常常是最重要的。

这些是服务器推送解决的问题。当您推送资源时,您将获得内联的实际好处,但您也可以将资产保留在保留其自己的缓存策略的外部文件中。但是,这一点有一个注意事项,在本文末尾进行了讨论。现在,让我们继续。

我已经讨论了为什么应该考虑使用服务器推送的原因以及它为用户和开发人员解决的问题。现在让我们谈谈如何使用它。

如何使用服务器推送

使用服务器推送通常涉及使用Link HTTP头,其格式如下:

Link: </css/styles.css>; rel=preload; as=style

请注意,我说的是通常。上面看到的实际上是预载入资源提示open in new window的实际应用。这是一种与服务器推送分离且不同的优化,但是大多数(不是全部)HTTP/2实现都将推送包含preload资源提示的Link头中指定的资产。如果服务器或客户端选择退出接受推送的资源,则客户端仍然可以启动资源的早期获取。

头文件中的as=style是必需的。它通知浏览器推送的资产的内容类型。在这种情况下,我们使用style的值来表示推送的资产是样式表。您可以指定其他内容类型。重要的是要注意省略as值可能会导致浏览器下载推送的资源两次。因此不要忘记它!

现在您知道如何触发推送事件,那么我们如何设置Link头呢?有两种方法:

  • 您的Web服务器配置(例如,Apache httpd.conf.htaccess);
  • 后端语言函数(例如,PHP的header函数)。

在服务器配置中设置Link

下面是一个示例,配置Apache(通过httpd.conf.htaccess)在请求HTML文件时推送样式表:

<FilesMatch "\.html$">
    Header set Link "</css/styles.css>; rel=preload; as=style"
<FilesMatch>

在这里,我们使用FilesMatch指令来匹配以.html结尾的文件请求。当请求符合此条件时,我们会在响应中添加一个Link头,告诉服务器推送位于/css/styles.css的资源。

副笔: Apache的HTTP/2模块也可以使用H2PushResource指令推送资源。该指令的文档说明称,此方法可以比使用Link头方法更早地启动推送。根据您的具体设置,您可能无法访问此功能。本文稍后展示的性能测试使用Link头方法。

截至目前,Nginx不支持HTTP/2服务器推送,而且到目前为止,在软件的更改日志中也没有表明已添加对其的支持。随着Nginx的HTTP/2实现成熟,这可能会改变。

在后端代码中设置Link

另一种设置Link头的方法是通过服务器端语言。当您无法更改或覆盖Web服务器的配置时,这非常有用。以下是如何使用PHP的header函数设置Link头的示例:

header("Link: </css/styles.css>; rel=preload; as=style");

如果您的应用程序驻留在共享托管环境中,无法修改服务器的配置,则此方法可能是您所拥有的全部内容。您应该能够在任何服务器端语言中设置此标头。只需确保在开始发送响应正文之前执行此操作,以避免潜在的运行时错误。

推送多个资源

到目前为止,我们所有的示例都只说明了如何推送一个资源。如果您想推送多个资源,怎么办?这样做是有道理的,对吧?毕竟,Web不仅由样式表组成。以下是如何推送多个资源:

Link: </css/styles.css>; rel=preload; as=style, </js/scripts.js>; rel=preload; as=script, </img/logo.png>; rel=preload; as=image

当您想推送多个资源时,只需使用逗号分隔每个推送指令即可。因为资源提示是通过Link标记添加的,所以这是您可以将其他资源提示与推送指令混合使用的语法。以下是将推送指令与preconnect资源提示混合使用的示例:

Link: </css/styles.css>; rel=preload; as=style, <https://fonts.gstatic.com>; rel=preconnect

多个 Link 标头也是有效的。以下是如何配置Apache以设置多个 Link 标头以请求HTML文档的方法:

<FilesMatch "\.html$">
    Header add Link "</css/styles.css>; rel=preload; as=style"
    Header add Link "</js/scripts.js>; rel=preload; as=script"
</FilesMatch>

这种语法比串联一堆逗号分隔的值更方便,并且它的作用也一样。唯一的缺点是它不太紧凑,但是方便程度超过了通过网络发送的一些额外字节。

现在你知道如何推送资源,让我们看看如何确定是否起作用。

如何确定服务器推送是否起作用

那么,你已经添加了“Link”头,告诉服务器推送一些东西。剩下的问题是,你如何知道它是否起作用了?

这取决于浏览器。近期版本的 Chrome 将在开发者工具的网络实用程序的启动程序列中显示推送的资源。

此外,如果我们在网络请求的瀑布图中将鼠标悬停在资产上,我们将获得有关资产推送的详细时间信息:

Firefox在识别推送的资产方面不太明显。如果一个资产已经被推送了,在开发人员工具中浏览器的网络实用程序中,它的状态将显示为一个灰点。

如果你正在寻找一种确定资产是否已被服务器推送的明确方法,你可以使用[nghttp命令行客户端](https://nghttp2.org/open in new window)来检查来自HTTP/2服务器的响应,如下所示:

nghttp -ans [https://jeremywagner.me](https://jeremywagner.me/)

这个命令将显示交易涉及的资产摘要。程序输出中,推送的资产旁边会有一个星号。

id  responseEnd requestStart  process code size request path
 13     +50.28ms      +1.07ms  49.21ms  200   3K /
  2     +50.47ms *   +42.10ms   8.37ms  200   2K /css/global.css
  4     +50.56ms *   +42.15ms   8.41ms  200  157 /css/fonts-loaded.css
  6     +50.59ms *   +42.16ms   8.43ms  200  279 /js/ga.js
  8     +50.62ms *   +42.17ms   8.44ms  200  243 /js/load-fonts.js
 10     +74.29ms *   +42.18ms  32.11ms  200   5K /img/global/jeremy.png
 17     +87.17ms     +50.65ms  36.51ms  200  668 /js/lazyload.js
 15     +87.21ms     +50.65ms  36.56ms  200   2K /img/global/book-1x.png
 19     +87.23ms     +50.65ms  36.58ms  200  138 /js/debounce.js
 21     +87.25ms     +50.65ms  36.60ms  200  240 /js/nav.js
 23     +87.27ms     +50.65ms  36.62ms  200  302 /js/attach-nav.js

这个网站 https://jeremywagner.me/open in new window 使用了nghttp,至少在编写时,它推送了五个资产。左侧的requestStart列上带有星号的资产是被推送的资产。

既然我们可以确定资产何时被推送,让我们看看服务器推送实际如何影响真实网站的性能。

测量服务器推送性能

测量任何性能增强的效果都需要一个好的测试工具。Sitespeed.ioopen in new window是一个可通过npmopen in new window获得的优秀工具;它自动化页面测试并收集有价值的性能指标。选择了适当的工具,让我们快速了解测试方法。

测试方法

我想以有意义的方式衡量服务器推送对网站性能的影响。为了使结果有意义,我需要在六个单独的场景下建立比较点。这些场景分为两个方面:使用HTTP/2或HTTP/1。在HTTP/2服务器上,我们想要衡量服务器推送对多个指标的影响。在HTTP/1服务器上,我们想要看到资产内联如何影响相同指标的性能,因为内联应该大致类似于服务器推送提供的好处。具体而言,这些情况如下:

  • 未使用服务器推送的HTTP/2:在此状态下,网站在HTTP/2协议上运行,但不推送任何内容。网站运行“默认”,可以说。
  • 仅推送CSS的HTTP/2:使用服务器推送,但仅用于网站的CSS。网站的CSS非常小,使用Brotli压缩open in new window后重量略高于2 KB。
  • 推送所有内容。推送网站上所有页面中使用的所有资产。这包括CSS,以及分布在六个资产上的1.4 KB JavaScript和分布在五个资产上的5.9 KB SVG图像。所有引用的文件大小再次是Brotli压缩后的大小。
  • 未内联资产的HTTP/1:网站在HTTP/1上运行,并且未内联任何资产以减少请求数量或增加呈现速度。
  • 仅内联CSS:仅内联网站的CSS。
  • 内联所有内容:网站上所有页面中使用的所有资产都已内联。CSS和脚本已内联,但SVG图像已使用base64编码并直接嵌入标记中。应注意,base64编码的数据大约比其未编码的等效数据大1.37倍open in new window

在每种情况下,我使用以下命令启动测试:

sitespeed.io -d 1 -m 1 -n 25 -c cable -b chrome -v [<https://jeremywagner.me>](<https://jeremywagner.me/>)

如果您想了解此命令的详细信息,您可以查看文档open in new window。简而言之,此命令使用以下条件测试我的网站主页https://jeremywagner.meopen in new window

  • 不爬取页面中的链接。仅测试指定页面。
  • 测试页面25次。
  • 使用类似“电缆”的网络限制配置。这相当于往返时间为28毫秒,下行速度为5000千比特每秒,上行速度为1000千比特每秒。
  • 使用Google Chrome运行测试。

从每个测试中收集并绘制了三个指标:

  • 首次绘制时间。这是页面在浏览器中首次可见的时间点。当我们努力使页面“感觉”快速加载时,这是我们要尽可能减少的度量标准。
  • DOMContentLoaded时间。这是HTML文档完全加载并已解析的时间。同步JavaScript代码将阻止解析器并导致此数字增加。使用async属性在<script>标记上可以帮助防止解析器阻塞。
  • 页面加载时间。这是页面及其资产完全加载所需的时间。

有了测试的参数,让我们看看结果!

测试结果

在早期指定的六种情况下进行了测试,并绘制了结果图。让我们首先看一下每种情况下首次绘制时间的影响:

让我们先谈一下图表的设置。蓝色部分代表平均首次绘制时间。橙色部分是90百分位数。灰色部分代表最大的首次绘制时间。

现在让我们谈谈我们看到了什么。最慢的情况是没有任何增强的HTTP/2和HTTP/1驱动的网站。我们确实看到,对于CSS使用服务器推送可以使页面平均渲染速度比完全不使用服务器推送快约8%,甚至比在HTTP/1服务器上内联CSS还快约5%。

然而,当我们尽可能地推送所有资源时,情况有所改变。首次绘制时间略有增加。在我们内联所有可能的内容的HTTP/1工作流中,我们实现了与推送资产相似的性能,尽管略微逊色。

这里的结论很明确:通过服务器推送,我们可以实现略好于在HTTP/1上内联时所能实现的结果。然而,当我们推送或内联许多资产时,我们观察到收益递减。

值得注意的是,使用服务器推送或内联都比完全没有增强对于首次访问者来说更好。值得注意的是,这些测试和实验是在具有小型资产的网站上运行的,因此此测试案例可能不反映您的网站可实现的情况。

让我们检查每种情况对DOMContentLoaded时间的性能影响:

这里的趋势与之前的图表没有太大不同,除了一个显著的例外:在HTTP/1连接上尽可能内联多个资产的情况下,DOMContentLoaded时间非常短。这可能是因为内联减少了需要下载的资产数量,使解析器可以在没有中断的情况下继续工作。

现在,让我们看看每种情况如何影响页面加载时间:

早期测量的已经确立的趋势在这里也普遍存在。我发现仅推CSS可以实现最大的加载时间效益。在某些情况下,推送太多的资源可能会使Web服务器有些缓慢,但仍然比不推任何东西要好。与内联相比,服务器推送比内联产生更好的总体加载时间。

在我们结束本文之前,让我们谈谈使用服务器推送时应该注意的一些注意事项。

使用服务器推送时的注意事项

服务器推送并不是您网站性能问题的万灵药。它有一些缺点,您需要注意。

您可以推送太多东西

在上面的一个场景中,我推送了很多资源,但它们总体上只占总数据的一小部分。一次推送太多非常大的资源实际上可能会延迟页面的绘制或更早地交互,因为浏览器不仅需要下载HTML,还需要下载推送的所有其他资源。您最好选择推送的内容。样式表是一个好的起点(只要它们不是大文件)。然后评估推送其他内容是否有意义。

您可以推送不在页面上的东西

如果您有访问者分析来支持这种策略,这并不一定是坏事。一个很好的例子可能是多页注册表单,您可以在注册过程的下一页中推送资源。但是,必须非常清楚:如果您不知道是否应该强制用户预先加载他们还没有看到的页面的资源,请不要这样做。有些用户可能处于受限数据计划中,您可能会花费他们的真实费用。

正确配置您的HTTP/2服务器

某些服务器为您提供了许多与服务器推送相关的配置选项。Apache的mod_http2有一些选项可用于配置如何推送资产。 H2PushPriority设置应该特别感兴趣,尽管在我的服务器的情况下,我将其保留为默认设置。一些实验可能会产生额外的性能收益。每个Web服务器都有一整套不同的开关和拨号可以供您尝试,因此请阅读您的手册并找出可用的内容!

推送可能无法缓存

关于服务器推送是否会影响性能的问题,可能会让访问者需要重新接收不必要的推送资源,这也让一些人担忧。一些服务器尽力缓解这个问题。Apache的mod_http2使用[H2PushDiarySize设置](https://httpd.apache.org/docs/2.4/mod/mod_http2.html#h2pushdiarysizeopen in new window)来优化这个问题。H2O服务器有一个名为Cache Aware server pushopen in new window的功能,它使用一个cookie机制来记住已推送的资源。

如果您不使用H2O服务器,您可以通过在缺少cookie的情况下仅推送资源来在您的Web服务器或服务器端代码中实现同样的功能。如果您有兴趣了解如何做到这一点,请查看我在CSS-Tricks上写的一篇文章open in new window。值得一提的是,浏览器可以发送一个RST_STREAM帧来向服务器发出信号,表示不需要推送的资源。随着时间的推移,这种情况将被更加优雅地处理。

尽管我们即将结束我们的时间,但让我们总结一下我们所学到的内容。

最后的想法

如果您已经将您的网站迁移至HTTP/2,那么您就没有理由不使用服务器推送。如果您有一个高度复杂的网站,有很多资产,请从小开始。一个好的经验法则是考虑推送您曾经可以放在内联中的任何东西。一个好的起点是推送您网站的CSS。如果您在这之后感觉更有冒险精神,那么可以考虑推送其他东西。始终测试更改以查看它们对性能的影响。如果您足够尝试这个功能,您可能会发现一些好处。

如果您没有使用类似H2O服务器的缓存感知的服务器推送机制,请考虑使用cookie跟踪您的用户,并仅在没有cookie的情况下向他们推送资源。这将最小化对已知用户的不必要推送,同时提高未知用户的性能。这不仅有助于性能,还向具有受限数据计划的用户表达了尊重。

上次编辑于:
贡献者: Shen Yuan