印前

HTTP/2 PUSH(推送)与HTTP Preload(预加载)大比拼| Dexecure

原文链接: dexecure.com

HTTP / 2 PUSH功能可以让服务器在没有相应的请求情况下预先将资源推送到客户端。HTTP预加载方法是指示浏览器加载当前页面所需资源。在这篇文章中,将讨论PUSH和Preload间的关键差别,也会给出不同情况下应使用哪种方式的详细说明。

想了解PUSH和Preload的优点,最简单的方式是通过网络请求的图表。假设浏览器正在加载一个简单的html页面,这个页面中有一个CSS文件,这个CSS文件中又引用了一个字体文件。下面是一个请求响应循环示意图的简版。

Normal case

若使用HTTP/2 PUSH,当服务端获取到HTML文件后,知道以后客户端会需要字体文件,它就立即推送这个文件给客户端。

Push case

设置Preload标记或头部可以请求以后会被使用的文件。这就是preload标记的作用。

Preload case

他们之间的相似点

他们的优化原理都是使用客户端未使用的带宽预先下载资源,这是种将下载与资源实际执行分离的好办法。例如,一个脚本文件可以被预加载或被推送到客户端,当浏览器真的请求这个脚本时,它会发现这个脚本就在缓存中,并不需要去等待网络。

它们的不同之处

语法

资源通过HTML可以被预加载

`<link rel="preload" href="/styles/other.css" as="style">`

也可以通过响应头预加载

 `Link: [https://example.com/other/styles.css](https://example.com/other/styles.css); rel=preload; as=style`

发起HTTP/2 PUSH取决于使用的服务端。例如,如果服务端是Node.js,就可以通过pushstream api这个api来开启一个PUSH流。

preload规范提到当服务端看到preload头部时可以开启PUSH功能(嗯,就是这样),而且大多数CDN都基于此进行标准化, 然后再从其他结点发起PUSH流。有时这让很困惑,但就像你在这篇文章看到的那样,PUSH和preload完全bu tong。只有极少数的情况下会交换使用它们。早已有关这个行为该不该被分离的讨论

目前,如果确定你只想预加载资源而不是推送资源,可以在Link标签的header里使用no-push属性, 像下面这样:

`Link: </app/style.css>; rel=preload; as=style; nopush`

资源类型

你只能给同源或者有推送权的源推送资源。但是,你却可以从任何源里preload资源。

PUSH/preload最早可启用的时间点

由于PUSH / Preload的主要目的是为了提前加载浏览器之后需要的资源,比较PUSH和preload应该何时开始加载或推送还是很有意思的。关于这个,PUSH比preload更好,因为可以服务端一收到客户端的第一个请求就可以发起推送。

Push time

只有当浏览器收到从服务端发来的HTML,解析后发现文件里有preload标签,才可以开启preload。几乎所有的浏览器都有lookahead parser,这对此功能非常有帮助。

通过HTTP头部来做Preload相比较以前的方法会有小的改善,因为不需要等到浏览器解析HTML时才开始preload的请求。

Early Hints在此事上已经更前进了一步.使用这个状态码,服务器甚至可以在发送HTML的响应头之前启动一个预加载(或一个PUSH)。Early Hints状态代码(103)最近被IETF批准为正式状态码。

Preload with early hints

缓存语法

被PUSH或preloaded的资源会用不同的资源类型来缓存。preload的资源是内存缓存的一部分,而被PUSH的资源会分别存储在PUSH缓存里(不对,找不到任何关于这个缓存的名字的提示)

这很重要,因为这两种缓存都有不同的语法,接下来让你亲眼见证吧。

Types of caches(Image credit)

缓存生命周期

PUSH缓存与HTTP/2连接紧密相连,当连接终止时缓存就会被清除。如果没有请求匹配PUSH缓存的任何一项,那它就不会被使用。由于,HTTP/2连接可以被多个标签间重用。PUSH的资源也可以被其他标签的请求声明。

然而,内存缓存是与发起请求的页面有关。所以也就注定了preloaded的请求不能在页面间共享。

与HTTP缓存的交互

当缓存里已经有要推送的资源时,服务端就不会再推送该资源了。理论上,浏览器应该发送RST_STREAM帧给服务端,终止PUSH流,因为浏览器缓存里已经有这个资源了。但是,由于各种各样的浏览器bug, 还不能像我们想要的那样工作。

即使当这些bug都修复了,问题也不能完全解决,因为服务端可以在收到客户端的RST_STREAM之前,终止推送大量的帧。缓存摘要是解决这个问题的提议。在浏览器实现前,你可以通过服务端来模拟他们的实现

另一方面,预加载与HTTP缓存配合得很好。若发现资源已经在HTTP缓存里了,那绝对不会再发网络请求了。

更多关于这些缓存类型的详情解释,请前往Yoav Weiss’s非常棒的文章查看。

资源优先级

浏览器在如何优化资源加载策略上已经花了大量的时间。通过preload,您可以附上要预加载的资源类型,这有助于浏览器找出资源的优先级。

而对于HTTP/2推送,这部分重担也给了服务端。浏览器在HTTP / 2连接中将不同的优先级附加到不同的流上。基于客户端发送的优先级,服务器将决定如何分配和决定资源优先级。当HTTP/2实现更加成熟后,基于服务端和客户端的优先算法,会有明显的提高的。

Javascript的Load和Error事件。

如果你使用link标签预加载资源, 你可以在该标签上设置onload(和onerror)属性,这样当资源被加载时会触发该事件。

对应用层来说HTTP/2 PUSH更加透明。已经有对服务端PUSH相关通知针对性措施和hacks。

内容协商

由于HTTP / 2预加载是简单的旧HTTP请求,因此可以基于不同的AcceptCookie和其他头,使用内容协商来确定发送给用户正确的资源。例如,Dexecure使用客户端提示加载图像的正确版本。由于客户端提示头部与图像请求一起发送,所以这可以很好地预先加载。由于使用HTTP / 2 PUSH,处理的是没有相应请求的资源,因此几乎不能使用内容协商。

浏览器支持情况

对预加载的支持现在还不是很好,而且大多数的HTTP/2 PUSH实现依然没有很优美。但值得庆幸的是可以在已经支持的浏览器上使用这些特性,而不支持的浏览器会直接忽略这个标记或者头(渐进增强)。不过也有使用preload特性的js方法, 只需要在页面中动态地加入标记。

Preload(预加载)支持情况

Preload support

PUSH(推送)支持情况

Push support

使用场景

PUSH(推送)

以下是仅可使用PUSH(推送)的例子。Preload(预加载)在这些场景中支持不是很好。

  • 利用服务器思考时间。通常,当服务器生成HTML页面时,网络处于空闲状态,这对于动态网页来说可能花费很多的时间。在这段时间中,浏览器不可以发起任何请求, 因为它没有意识到页面可能会引用子资源。这时就可以利用这段时间推送资源到客户端。这还具有预热连接和增加TCP拥塞窗口大小的额外好处,以便将来的请求可以更快地完成。前往这个方便的网站查看你的网站是否可以利用这种技术。

  • PUSH(推送)资源,习惯上推送内联资源,如小图像,CSS和JS。这是PUSH彻底消除的一个漏洞。当将其作为单独资源进行外部化并推送时,可以更好地利用浏览器缓存。当你内联时,不必要每次都把资源和HTML一起发送。

Preload(预加载)

除了上面的那些情况,我们都建议使用Preload(预加载)。Preload(预加载)对于浏览器解析器发现得晚的资源来说是非常好的,这对页面渲染是至关重要的。以下是筛选的预加载资源的一些主要应用场景示例。

  • CSS文件引用的字体

  • 外部CSS文件中通过background-url加载的图片

  • 内联关键CSS(或JS),或是预加载其余的CSS(或JS)

那么Websocket,SSE,WEBRTC,Prefetch,Prerender和Subresource呢?

Websocket,服务器发送事件(SSE)和WebRTC被用来向用户提供实时数据(有时来自用户的数据)。在资源层面上使用PUSH和Preload会更好。

Prefetch用于向浏览器指示下载未来需要的资源。例如,用户尚未访问的页面所需的webpack捆绑包就是很好的需要prfetch的候选项。另一方面,如前所述,预加载资源在页面导航中丢失。

Prerender,现在已经被弃用了, 它曾经用于获取、渲染用户将来一定会访问的页面。

Subresource,现在不赞成使用,以前有些基本方法(已经不用了)

总结

那用户现在该干嘛呢?我做了个流程图,帮助快速决定使用哪种方法。注意:我没有包含内容协商和js 的error、onload事件。因为如果网站使用服务器来做,就不存在这两个问题了。

Preload support