[译] HTTP/2 Server Push 详解

[译] HTTP/2 Server Push 详解

原文:smashingmagazine.com/20

作者:Jeremy Wagner

译者按:网络优化一直是译者长期研究的方向,HTTP/2 的理论学习也已做了不少,随着这项标准的推进,越来越多特性被大家开始使用。作为 HTTP/2 最激动人心的特性,Server Push 在性能提升的效果被寄予了很高期望,却因其对传统 B/S 架构的开发模式影响较大未能广泛实践。如何更好地使用这项能力,让我们跟着作者深入探索~

========================译文分割线===========================

在过去的一年时间,HTTP/2 的出现为关注性能的开发者带来了显著的变化。HTTP/2 已经不再是我们期待中的特性,而是伴着 Server Push(服务端推送)能力已然到来。

除了解决常见的 HTTP/1 性能问题(比如,首部阻塞和未压缩的报头),HTTP/2 还提供了 Server Push 能力!服务端推送允许我们向用户发送一些还没有被访问的资源。这是一种获得 HTTP/1 优化实践(例如内联)所带来性能提升的优雅方式,同时也避免了原先实践的一些缺点。

本文中,你将了解什么是 Server Push,它的工作原理与解决了哪些问题。同时你也将学习如何使用它,判断它是否正常运作,以及它对性能的影响。让我们开始吧!

Server Push 为何物

访问网站始终遵循着请求——响应模式。用户将请求发送到远程服务器,在一些延迟后,服务器会响应被请求的内容。

对网络服务器的初始请求通常是一个 HTML 文档。在这种情况下,服务器会用所请求的 HTML 资源进行响应。接着浏览器开始对 HTML 进行解析,过程中识别其他资源的引用,例如样式表、脚本和图片。紧接着,浏览器对这些资源分别发起独立的请求,等待服务器返回。

典型的服务器通信(大图)

这一机制的问题在于,它迫使用户等待这样一个过程:直到一个 HTML 文档下载完毕后,浏览器才能发现和获取页面的关键资源。从而延缓了页面渲染,拉长了页面加载时间。

有了 Server Push,就有了解决上述问题的方案。Server Push 能让服务器在用户没有明确询问下,抢先地“推送”一些网站资源给客户端。只要正确地使用,我们可以根据用户正在访问的页面,给用户发送一些即将被使用的资源。

比如说你有一个网站,所有的页面都会在一个名为 styles.css 的外部样式表中,定义各种样式。当用户向务器请求 index.html 时,我们可以在发送 index.html 的同时,向用户推送 styles.css。


使用HTTP/2 Server Push的Web服务器通信(大图

相比等待服务器发送 index.html,然后等待浏览器请求并接收 styles.css,用户现在只需等待1次服务器响应,就可在初次请求同时使用 index.html 和 styles.css。

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

Server Push 解决了什么问题?

Server Push 解决了减少关键内容的网络回路耗时问题,但这并不是唯一的作用。Server Push 更像是 HTTP/1 特定优化反模式的替代方案,例如将 CSS 和 JavaScript 内联在 HTML,以及使用 data URI 方案将二进制数据嵌入到 CSS 和 HTML 中。

这些技术在 HTTP/1 优化工作流中非常受用,是因为这样减少了我们所说的页面“感知渲染时间”,也就是说在页面整体加载时间可能不会减少的同时,对用户而言网页的加载速度却显得更快。这确实是说得通的,如果你将 CSS 内嵌到 HTML 的<style>标签中,浏览器就可以无需等待外部资源的获取,而立即应用 HTML 中的样式。这种概念同样适用于内联脚本,以及使用 data URI 方式内联二进制数据。

内联内容的服务器通信(大图

这看起来是个不错的方案,对吧?在 HTTP/1 的时代确实如此,因为也没有别的选择。而这么做实际上也留下了恶果,即内联的内容不能有效地被缓存。若样式、脚本资源以外链及模块形式引用,会更高效地进行缓存。当用户访问后续页面需要这些资源时,可以直接从缓存中获取,从而省去了额外的资源请求。

优化缓存行为(大图

而当我们对内容进行内联时,它们是没有独立的缓存上下文的,仅存在于所内联文档的上下文中。举个在 HTML 中内联 CSS 的例子,如果 HTML 的缓存策略,是每次访问都向服务器拉取最新的内容,那么内联的CSS总是无法缓存其内容。即使把 HTML 进行缓存,但在后续访问的页面内,内联相同的 CSS 内容也是需要重复下载的。这还是比较宽松的缓存策略,实际情况中 HTML 仅有较短的缓存周期。内联是我们在 HTTP/1 优化方案中所做的权衡,它确实在用户第一次访问时非常有效,而往往第一印象是非常重要的。

这就是 Server Push 能解决的问题。当推送资源时,我们能获得与内联相同的性能提升,同时保持资源的外链形式,从而有独立的缓存策略。这里有个需要注意的问题,我们稍后再深入探讨。

我已经谈了很多为什么你该考虑使用 Server Push 的原因,也澄明了它能为用户和开发者所解决的问题。接下来让我告诉你如何去使用它。

如何使用 Server Push

使用Server Push,通常会以下面的方式使用 Link 这个HTTP首部。

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

注意我说的是通常,上面看到的实际是预加载资源示意(resource hint)的实践。这是个区别于 Server Push 的独立优化方案,但大多数(并非全部)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 首部方法启用推送。根据 Apache 安装时的不同设置,你也可能无法使用此功能。本文后面会给出 Link 首部方法的性能测试结果。

截至目前,Nginx 并不支持 HTTP/2 Server Push,目前的 changelog 中没有任何支持情况的记录。而随着 Nginx HTTP/2 实现的逐渐成熟,这种情况可能会发生变化。

使用后端代码设置 Link 首部

另一个设置 Link 首部的方法是使用服务器端语言。这在你无法修改或覆盖服务器配置时十分有效。下面是 PHP header 方法设置 Link 首部的例子:



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

如果你的应用程序部署在一个共享的托管环境中,并且修改服务器的配置不太现实,那么这个方法可能是最适合你的。你可以使用任何服务端语言设置这个首部。在真实使用前记得确保测试无误,以避免潜在的运行时错误。

多资源推送

目前看到的都是演示推送一个资源的例子,如果想一次推送更多资源呢?这么做也是很有道理的,对吧?毕竟页面不止是样式表组成的。下面来看推送多资源的例子:



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 给 HTML 配置多个 Link 首部的例子:



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

这种语法相比一长串逗号分隔的字符串更为方便,且达到的作用是相同的。唯一的缺点就是没那么紧凑,而且会多一点字节量的网络传输,但提供的便利是值得的。

现在知道了如何推送资源,我们继续看推送是否生效。

如何分辨 Server Push 是否生效

目前,我们已经通过 Link 首部来告诉服务器推送一些资源。剩下的问题是,我们怎么知道是否生效了呢?

这还要看不同浏览器的情况。最新版本Chrome将在开发者工具的网络发起栏中展示推送的资源。

Chrome显示服务器推送的资源(大图

更进一步,如果把鼠标悬停在网络请求瀑布图中的资源上,将获得关于该推送资源的详细耗时信息:

Chrome显示推送资源的详细耗时信息(大图

Firefox对推送资源则标识地没那么明显。如果一个资源是被推送的,则浏览器开发者工具的网络信息里,会将其状态显示为一个灰色圆点。

Firefox对推送资源的展示(大图

如果你在寻找一个确保能分辨资源是否为推送的方法,可以使用 nghttp 命令行客户端来检查是否来自 HTTP/2 服务器,像这样:



nghttp -ans 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

这里,我在自己的站点上使用了 nghttp,有五个推送的资源(至少在写这篇文章时)。推送的资源在 requestStart 栏左侧以星号标记了出来。

现在我们知道了如何识别推送的资源,接下里具体看看对真实站点的性能有什么实际影响。

测量 Server Push 性能

测量任何性能提升的效果都需要很好的测试工具。Sitespeed.io 是一个可从 npm 获取的优秀工具,它可以自动地测试页面,收集有价值的性能数据。有了得力的工具,我们来快速过一下测试方法吧。

测试方法

我想通过一个有意义的方法,来测量 Server Push 对网站性能的影响。为了让结果是有意义的,我需要建立6种独立的场景来交叉对比。这些场景主要以两个方面进行分隔:使用 HTTP/2 或 HTTP/1。在 HTTP/2 服务器上,我们想测量 Server Push 在多个指标的效果。在 HTTP/1 服务器上,我们想看看内联资源的方法,在相同指标中对性能有什么影响,因为内联应该能达到和 Server Push 差不多的效果。具体场景如下:

  • 未使用 Server Push 的HTTP/2

网站使用了 HTTP/2 协议,但没有资源是被推送的。

  • 仅推送 CSS 的 HTTP/2

使用了 Server Push,但仅用在了 CSS 资源。该网站的 CSS 体积比较小,经过 Brotli 压缩后仅有2KB多一点。

  • 推送所有资源

网站的所有资源都是推送的。包括了上面的 CSS,以及6个JS(合计 1.4KB)、5个SVG图片(合计5.9KB)。这些资源同样经过了压缩处理。

  • 未内联资源的HTTP/1

网站只运行在 HTTP/1 上,没有内联任何资源,来减少请求数和加快渲染速度。

  • 只内联 CSS

只有网站的 CSS 被内联了。

  • 内联所有资源

页面上的所有资源都进行了内联。CSS 和脚本是普通内联,而 SVG 图片是经过 Base64 编码方式直接放入 HTML 标签中。值得一提的是 Base64 编码后体积比原先大了1.37倍

在每个场景中,都使用下面的命令开始测试:



sitespeed.io -d 1 -m 1 -n 25 -c cable -b chrome -v https://jeremywagner.me

如果想知道这个命令的输入、输出,可以参看文档。简而言之,这个命令测试了我的网站 Home - Jeremy Wagner 的主页,使用了下面的条件:

  • 页面中的链接无法抓取。只测试指定的页面。
  • 页面测试25次
  • 使用了“有线宽带”级的网络配置。回路时间(译者注:RTT)为28ms,下行带宽是5000kbps,上行带宽为1000kbps。
  • 测试使用 Google Chrome

每项测试中收集和展示3项指标:

  • 首屏渲染时间

页面在浏览器首次展现的时间点。当我们努力让一个页面“感觉上”加载很快时,那么这个指标是我们要尽量降低的。

  • DOMContentLoaded 时间

这个是 HTML 完成加载与解析的时间。同步的 JavaScript 代码会阻塞解析,并导致这个时间增加。在<script>标签上使用 async 属性可以避免对解析的阻塞。

  • 页面加载时间

这个是整个页面完成所有资源加载的耗时。

测试的所有因素都确定后,让我们看看结果!

测试结果

经过对上述6种场景的测试,我们将结果以图表形式做了展示。先看看各个场景的首屏渲染时间情况:

首屏渲染时间(大图

让我们先讲讲图表是如何设计的。图中蓝色部分代表了首屏渲染的平均时间,橙色部分是90%的情况,灰色部分代表了首屏渲染的最长耗时。

接下来我们讨论结果。最慢的情形是未使用任何优化的 HTTP/2 和 HTTP/1。可以看到,对 CSS 使用 Server Push 使页面渲染平均速度提升了8%,而内联 CSS 也比简单的 HTTP/1 提升了5%速度。

当我们尽可能地推送了所有资源,图片却显示出了一些异样,首屏渲染时间有所轻微增加。在 HTTP/1 中我们尽可能内联所有资源,性能表现和推送所有资源差不多,仅仅少了一点时间。

结论很明确:使用 Server Push,我们能获得比 HTTP/1 中使用内联更优的性能。但随着推送或内联的资源增多,提升的效果逐渐减少。

使用 Server Push 或内联虽好,但对于首次访问的用户并没有太大价值(译者注:实际上对于首次访问用户有很大的性能提升,猜测作者这里笔误了)。另外,这些测试实验是运行在较少资源的站点上,所以未必能反映出你的网站的使用情况。

我们再看看各项测试对 DOMContentLoaded 时间的影响:

DOMContentLoaded 时间(大图

数据趋势跟刚才看到的图表没太大差别,除了一个需要注意的区别:在 HTTP/1 中尽可能地内联资源,相对 DOMContentLoaded 时间非常低。可能的原因是内联减少了需要下载的资源数,从而保证解析器(parser)可以不被打断地工作。

最后再看看页面加载时间的情况:

页面加载时间(大图

各项测量数据依然保持了先前的趋势。仅推送 CSS 时加载时间最短。推送所有资源会偶尔导致服务迟缓,但毕竟还是比什么都不做表现更优。与内联相比,Server Push 的各项情况都是优于内联的。

在做最后总结前,还要讲讲使用 Server Push 时可能遇到的问题。

使用 Server Push 的一些建议

Server Push 并不是性能优化的万金油,它也有一些需要注意的地方。

推送过多资源

前面的一项测试中,我推送了很多资源,但它们加起来也只占传输数据的一小部分。一次推送很多大资源的话,会造成页面渲染及可交互时间的延迟,因为浏览器不但要加载 HTML 文档,还要同时下载推送的资源。最好的做法是有选择性地推送,样式表文件是个不错的开始(目前它们并不是很大),接着再评估还有什么其他资源适合推送。

推送页面以外的资源

如果你有访客统计分析,那么这种做法也未必不好。一个好的例子是,在多页注册账户表单场景,可以推送下一页的注册步骤资源。但要澄清的是,如果你不确定用户是否会访问后续的页面,千万不要尝试推送它的资源。有些用户的流量是十分珍贵的,这么做可能会导致其不必的损失。

正确地配置 HTTP/2 服务

有些服务器会给出很多 Server Push 的配置选项。Apache 的 mod_http2 模块有一些关于如何推送资源的配置选项。H2PushPriority 设置就比较有意思,虽然在我的服务器上使用了默认设置。有一些实验性的配置可以获得额外的性能提升。每一种 Web服务器都有其整套不同的实验性配置,所以查看你的服务器手册,看看有哪些配置可以用起来吧!

推送资源可能不被缓存

Server Push 也有一些有损性能的的情况,对于访问网站的回头客们,一些资源可能会被非必要地进行推送。有些服务器会尽可能地减轻这种影响。Apache 的 mod_http2 模块使用了 H2PushDiarySize 设置对这一点进行了一些优化。H2O 服务器有一种 Server Push 缓存感知特性,使用了 Cookie 机制来记录推送行为。

如果你不是使用 H2O服务器,也可以使用服务端代码实现同样的效果,即只推送 Cookie 记录外的资源。如果有兴趣了解具体做法,可以查看我在 CSS Tricks 上的文章。值得一提的是,浏览器可以向服务器发送一个 RST_STREAM 帧来通知不需推送的资源。随着时间推移,这个问题的解决将会愈加优雅。

最后来总结一下以上学到的内容。

最后的思考

如果你已经将自己的网站迁移到 HTTP/2,你没有什么理由不使用服务器推送。如果你的网站因有过多的资源而显得复杂,可以从体积较小的资源开始尝试。一个好的经验法则是,考虑推送那些你曾经用到内联的资源。推送 CSS 是个不错的开始。如果感觉更有冒险精神之后,就考虑推送其他资源。要牢记在改动后测试对性能的影响。下了一定功夫后,你一定能从中有所受益。

如果你没有用像 H2O 这样使用缓存感知推送机制的服务器,可以考虑用 cookie 追踪你的用户,只在没有相关 cookie 的情况下给他们推送资源。这样可以为未知用户提升着性能的同时,最小化向已知用户的资源推送量。这不仅利于性能优化,也向用户展示了数据用量的尊重。

剩下的就需要你自己在服务器上折腾 Server Push 了,看看有哪些特性可以对你或用户有用吧。如果你想了解更多关于 Server Push,看看这些资源吧:


编辑于 2017-04-23 01:05