技术 ·

如何使用Preload预加载技术优化网站速度

在网站建设中我们可能通过预加载技术来缩短网站的加载时间来提高用户的体验,本文包括对概览Prefetching, Preloading, Prebrowsing技术的概览和对Preload用法和规范的讲解

概览Prefetching, Preloading, Prebrowsing技术

本文翻译自多个来源,为了阅读通畅,部分技术术语就不做蹩脚的翻译了。原文中有很多扩展文章的链接,都是对所在章节的详细说明,个个都值得一读。

当我们谈到前端的性能时,总是会提到比如合并、压缩、缓存或者在服务器上开启gzip之类的,目的都是为了让页面加载的更快。

资源预拉取(prefetch)则是另一种性能优化的技术。通过预拉取可以告诉浏览器用户在未来可能用到哪些资源。

Patrick Hamann 对此的说明

Pre-fetching会提示浏览器那些未来一定或可能使用到的资源,有时在当前页面见效,有些则在未来可能打开的页面生效。 作为开发者,我们比浏览器更懂自己的应用。我们可以利用这些技术提前告知浏览器web中用到的核心资源。

以前这种实践也被称为『prebrowsing』。但这并不是一种单一的技术,实际上可以拆分成很多小点:dns-prefetch, subresource, prefetch, preconnect, 和 prerender.

DNS prefetch

DNS prefetching通过指定具体的URL来告知客户端未来会用到相关的资源,这样浏览器可以尽早的解析DNS。比如我们需要一个在example.com的图片或者视频文件。在<head>就可以这么写:

当请求这个域名下的文件时就不需要等待DNS查询了。项目中有用到第三方的代码时这么做尤其有益(译者注:其他的使用场景,比如当静态资源和HTML不在一个域上,而在CDN上;又比如在重定向前可以加上DNS prefetch)。

Harry Roberts在他的前端性能优化文章中建议:

简单的一行就能让支持的浏览器提前解析DNS。也就是说在浏览器请求资源时,DNS查询就已经准备好了。

这可能看起来是个非常微不足道的性能提升,而且还不是必须的–Chrome总是会做类似的处理,用户只要在地址栏敲入一部分域名,如果命中了历史常用的网站,Chrome就会提前解析DNS、预拉取页面。(译者注:效果确实有限,但是聊胜于无)

caniuse

Preconnect

和DNS prefetch类似,preconnect不光会解析DNS,还会建立TCP握手连接和TLS协议(如果需要)。用法如下:

Ilya Grigorik写了一篇文章详细说明了这种技术:

现代浏览器竭尽所能的尝试预测网站可能需要哪些链接。通过提前连接,浏览器可以提前建立必要的通信,消除了实际请求中DNS、TCP和TLS的耗时。不过,即使是只能的现代浏览器,也没办法为每个网站可靠的预测所有连接。

幸运的是开发者可以告诉浏览器哪些通信需要在实际请求发起前就提前建立连接。

举个栗子: 上半张图显示了浏览器先拉html、再拉CSS并建立好CSSOM后,发现需要两个外链的字体(在fonts.gstatic.com上),然后浏览器开始发起两个请求,具体来说,需要对这个域进行DNS解析、TCP和TLS握手(一个建立后可以复用给另一个连接)。

下半张图增加了上面的代码来从fonts.gstatic.com preconnect资源。可以看到,浏览器在请求CSS的同时并行的建立字体资源需要的连接,等到真正开始需要字体时立刻就开始返回数据。

更多详细的内容可以参考Ilya Grigorik的文章。

目前只支持Firefox 39+和Chrome 46+,具体参见caniuse

Prefetch

当能确定网页在未来一定会使用到某个资源时,开发者可以让浏览器提前请求并且缓存好以供后续使用。prefetch支持预拉取图片、脚本或者任何可以被浏览器缓存的资源。

<link rel="prefetch" href="image.png">

不同于DNS prefetch,上面的写法可是会去请求、下载资源并且缓存起来。当然也是有一些发生条件的。比如,客户端可能会在弱网络下不去请求较大的字体文件,Firefox则只会在浏览器空闲的时候prefetch资源(译者注:这里是MDN上对浏览器空闲的定义和一些FAQ,建议阅读)。

正如Bram Stein在他的文章中指出,prefetch很适用于优化webfonts的性能。以前,字体文件必须等DOM和CSSOM创建好后才能下载,可如果prefetch了字体,这个瓶颈就能轻松解决了。

注意:prefetch并没有同域的限制

caniuse

Subresource

subresource可以用来指定资源是最高优先级的。比如,在Chrome和Opera中我们可以加上下面的代码:

<link rel="subresource" href="styles.css">

Chromium的文档这么解释:

和 "Link rel=prefetch"的语义不同,"Link rel=subresource"是一种新的连接关系。rel=prefetch指定了下载后续页面用到资源的低优先级,而rel=subresource则是指定当前页面资源的提前加载。

所以,如果资源是在当前页面需要,或者马上就会用到,则推荐用subresource,否则还是用prefetch。

Prerender

prerender是一个重量级的选项,它可以让浏览器提前加载指定页面的所有资源。

<link rel="prerender"  href="/thenextpage.html" />

Steve Souders的文章详细解释了这个技术:

prerender就像是在后台打开了一个隐藏的tab,会下载所有的资源、创建DOM、渲染页面、执行JS等等。如果用户进入指定的链接,隐藏的这个页面就会进入马上进入用户的视线。Google Search多年前就利用了这个特性实现了Instant Pages功能。微软最近也宣布会让Bing在IE11上用类似prerender的技术。

但是要注意,一定要在十分确定用户回点某个链接时才用这个特性,否则客户端就会无端的下载很多资源和渲染这个页面。

正如任何提前的动作一样,预判总是有一定风险出错。如果提前的动作是昂贵的(比如高CPU、耗电、占用带宽),就要谨慎使用了。虽然不容易预判用户会点进哪个页面,但还是存在一些典型的场景:

  • 如果用户搜索到了一个明显正确的结果时,那么这个页面就很有可能被点入
  • 如果用户在登录页面,那么登录成功后的页面就很可能接下来会被加载了
  • 如果用户在阅读一个多页面的文章或者有页码的内容时,下一页就很可能会马上被点击了

利用Page Visibility API可以用来防止页面在还没真正展示给用户时就触发了JS的执行。

caniuse

未来:Preload

以上是已有的技术,我们再谈谈未来。 preload草案建议允许始终预加载某些资源,不像prefetch有可能被浏览器忽略,浏览器必须请求preload标记的资源。

然而,这项草案还没有任何浏览器支持,不过值得关注。

 

总结

预判用户的操作虽然不易,而且还需要大量的设计和测试工作,但是性能的提升是值得我们孜孜不倦的去追求的。如果我们愿意试验这些预加载技术,我们肯定能显著地提升用户体验。

Preload的用法和优点

原文:https://www.smashingmagazine.com/2016/02/preload-what-is-it-good-for/

作者:Yoav Weiss

译者按:网络优化一直是译者长期研究的方向,除了近期热门的HTTP/2之外,还是要关注浏览器在加载策略上的一些改进,从不同层面提升用户的访问体验。prefetch这些HTML5的新特性,虽然很新鲜,但并未在生产环境中得到广泛使用,其中的原因是什么?preload有什么改进?本文将娓娓道来~

Preload规范)是一项新的Web标准,旨在提升性能,让Web开发者对加载的控制更加粒度化。它让开发者有自定义加载逻辑的能力,免受基于脚本的loader所带来的性能损耗。

几周前,我在Chrome Canary提交了对preload的支持,解决了一些bug,预计将在四月中旬合入Chrome稳定版。但preload到底是什么?它有什么用处?对你有什么好处呢?

好吧,<link rel="preload">它是一种声明式的获取(fetch)指令。

用人话来讲,它是一种告诉浏览器开始获取一项确定资源的方式,因为我们是作者(或者服务器管理员,或者聪明的服务器开发),我们是知道浏览器将很快需要那一项资源的。

不是已经有这种能力了吗?

嗯,也不算是真有。<link rel="prefetch">已经被支持一段时间了,也有不错的兼容性。在此基础上,Chrome也对<link rel="subresource">支持好一段时间了,所以preload有什么新特性?它跟前面这几个指令有什么区别?它们都告知浏览器去获取资源,是吧?

确实是的,但是它们有一些重要的区别,这些区别使得preload成为一项全新的指令,做出之前指令所不能做到的事情。

<link rel="prefetch">是一种告诉浏览器获取一项可能被下一页访问所需要的资源方式。这意味着资源将以较低优先级地获取(因为浏览器知道当前页面所需要的资源,要比我们猜测在下一页访问所需资源更重要)。这意味着prefetch的主要用途是加速下一页访问速度,而不是当前页面的速度。

<link rel="subresource">原本是计划处理当前页面的,但是在一些特别的场景失败了。由于Web开发者无法定义资源的优先级,所以浏览器(实际只有Chrome和基于Chromium的浏览器)使用同等较低的优先级加载资源,也就是说大多数情况,即使没有subresource,请求也是同一时机发出的。

Preload如何做得更好?

Preload像subresource一样,注定是为当前页面服务的,但具备一项小而重要的区别。它有一个 as 属性,这可以让浏览器做到很多subresource和prefetch做不到的事情:

  • 浏览器可以设置正确的资源优先级,使得资源可以被正确地加载,重要的资源不再会被延迟,不再被不重要的资源阻塞。
  • 浏览器会保证请求对应正确的内容安全策略(Content-Security-Policy )指令,不会发起非法请求。
  • 浏览器会基于资源类型发送正确的 Accept 首部。(比如获取图片时指定对“image/webp”的支持)
  • 浏览器知道资源的类型,所以可以稍后决定资源是否在后续请求中保持可重用。

Preload的另外一个不同是,它有onload事件(至少在Chrome中,对另外两种 rel 取值并没作用)。

Preload不阻塞window的onload事件,除非该资源是被一个阻塞该事件的资源请求的。

将这些特性结合在一起,我们可以做到一些新的事情。

加载较晚发现的资源

preload最基本的使用方式是提前加载较晚发现的资源。虽然大部分基于标签的资源会被浏览器内部的预加载器(preloader)提早发现,但并非所有资源都是基于标签的。有些资源是隐藏在CSS和JavaScript中的,浏览器不知道页面即将需要这些资源,而等到发现它们时已经为时已晚。所以在有些情况,这些资源延缓了首屏渲染,或是延缓了页面关键部分的加载。

而现在你有途径告诉浏览器,“嘿,浏览器!这个是你一会所需要的资源,现在就开始加载吧。”

做法可能如下

<link rel="preload" href="late_discovered_thing.js" as="script">

as 属性告诉浏览器什么类型的资源将被下载。as 可能的取值有:

  • “script”
  • “style”
  • “image”
  • “media”
  • “document”

(参考fetch规范的完整列表)

如果省略 as 属性,或者给定一个非法值,将等同发起一个XHR请求,因为浏览器不知道将要获取的是什么,并且加载的优先级也会很低。

较早加载字体

一种流行的“较晚发现关键资源”的代表是Web字体。一方面,它对页面渲染字体很关键(除非你在使用最新的font-display)。另一方面,它们被埋在CSS很深的地方,即使浏览器的预加载器解析了CSS,也不能保证在它们选择器应用到DOM节点之前,就是被需要的。理论上讲,浏览器应该可以发觉这种情况,但实际上都没有,而就算可以,也会造成无谓的下载,因为后续的CSS规则可能会覆盖现有的字体样式规则。

简而言之,这很复杂。

但是,你可以对一定需要的字体使用preload指令,摆脱上述的复杂问题。像这样:

<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>

有一点需要指明,获取字体时必须加上crossorigin属性,就如使用CORS的匿名模式获取一样。是的,即使你的字体与页面同域,抱歉……

同时,type属性确保了浏览器只预加载其支持的类型文件。现在,只有Chrome支持preload,同时支持WOFF2。未来会有越来越多浏览器支持preload,但我们不敢确保它们都会支持WOFF2。同理,不同浏览器对不同类型的资源支持也可能是不一样的。

动态加载,不做运行

另一个突然变得现实的有趣场景是,你想下载一个资源因为你觉得它是被需要的,但不想立即运行它。举个例子,想象这个场景,你想在页面生命周期的一个特定时间点执行代码,而不用通过脚本来控制(不用添加一个runNow()的方法)。

如今,要做到上面的事情非常受限。如果只是在需要运行的地方注入脚本,浏览器会在执行前下载这段脚本,耗费一点时间。也可以先使用XHR下载脚本,但浏览器会拒绝使用它,因为资源不是以同样type下载的。

所以该怎么办呢?

在没有preload之前,也没什么办法了。(在一些场景可以使用eval()脚本的内容,但那不太切合实际并且伴有副作用。)但可以配合preload来实现!

var preload = document.createElement("link");
link.href = "myscript.js";
link.rel = "preload";
link.as = "script";
document.head.appendChild(link);

可以在页面加载的早期运行它,也就是在希望脚本运行的早些时间(但你要确信这个脚本不会对其他需要加载的关键资源有所干扰)。然后但你想运行它们的时候,只要简单注入一段脚本就好了。

基于标签的异步加载器

另一个酷炫的技巧,是使用onload处理函数来创建一些基于标签的异步加载器。Scott Jehl做了这方面的第一个实验,作为他的loadCSS库。简单说,你可以这么使用:

在标签里获取异步的样式表!Scott还有一个该特性的demo页面。

该特性同样可以应用在脚本上。

话说我们不是已经有<script async>了吗?嗯,<script async>确实不错,但它会阻塞window的onload事件。在一些情况,这可能是你希望的,而有些情况并不是。

比如说你想下载一个上报分析脚本,希望尽快去加载它(以避免分析脚本漏掉一些访客上报),但不想它对用户体验有任何影响,也就是不希望它对onload造成延迟。(你可以说onload不是影响用户的唯一因素,但缩短转菊花的时间总是一件好事)。

有了preload,实现起来就很容易了:

(onload属性中包含太长JS可能不是个好主意,可以在inline的代码中来定义。)

响应式加载(RESPONSIVE LOADING)

因为Preload是一个链接,遵循规范它应有media属性(目前Chrome还未支持,不过很快就可以了)。这个属性可以启用资源的条件加载能力。

它又有什么用处呢?举个例子,你的网站的初始视窗,对于PC/宽屏设备展示可交互的地图版本,而对于手机/窄屏设备则展示静态的地图版本。如果你擅于加载性能优化,会想到在特定设备只加载其中一个版本的资源,而不是所有资源。而要做到这样唯一的办法就是使用JS去动态地加载资源。但是这么做,会使得资源对于preloader(译者注:上文提到过的浏览器内部的预加载器)不可见,因此会使得资源的加载时机稍微滞后,不但影响了用户的视觉体验,还对站点的SpeedIndex 得分有着负面影响

所以我们该怎么做,才能让浏览器尽早知道所需加载的资源呢?没错,就是Preload!

我们可以使用Preload提前加载这些资源,并且使用media属性,做到只加载需要的资源:

HTTP首部(Headers)

link标签带来另外一个特性就是,它可以代表一个HTTP的首部。这意味着上面很多基于标签的例子,你都可以使用HTTP响应首部达到同样的目的。(唯一的例外是onload相关的例子,你无法在HTTP首部中定义一个onload的handler。)

HTTP响应首部的例子:

当做优化的人与当初书写标签的人,不是同一个人时,HTTP首部可谓信手拈来。特别明显的例子是,当一个外部优化引擎,对内容进行扫描和优化的时候(透露下,我正在实现的一个)。

另一些例子是,一个独立的优化小组,想做这样的优化,或者是优化构建流程,避免了对HTML的修改,显著地降低了复杂度。

特性检测(Feature Detection)

最后一点:在上面的一些例子中,我们依赖的前提是preload支持基础的功能,如样式、脚本的加载。而如果浏览器中并不是如此,会怎么样呢?

会悲剧。。

我们不希望这样。由于preload的原因,我们改变了DOM规范,使得被支持rel关键字的特性检测成为了可能。

一个特性检测方法的例子:

这使得你可以提供降级加载机制,以防止因为preload缺乏支持,而站点发生崩溃的情况。So easy…

HTTP/2的Push涵盖了上面所有case吗?

并不是的,虽然它们部分特性有所重叠,但它们是互相配合的关系。

HTTP/2 Push的优势是,它可以推送浏览器还未发起请求的资源。也就是说,Push可以随时推送资源,甚至在HTML的请求还没被发送给到服务器的时候。它还可以被用来向下给一个开放的,不需要响应的HTTP/2链接推送资源。

而另一方面,preload可以解决一些HTTP/2不能处理的问题。如我们所见,使用Preload的应用知道资源加载的发生时机,并可在资源加载完毕后马上得到通知。这些并不是HTTP/2 Push所设计的目标。并且,HTTP/2 Push不能被第三方资源使用,而Preload在这方面是没有第三方之分的。

此外,HTTP/2 Push不能把浏览器的缓存和非全局Cookie状态考虑在内。虽然缓存状态可能可以通过新的cache digest specification规范解决,但对于非全局Cookie状态则无计可施了,所以Push不能使用在对这些Cookie有依赖的资源上。对于这类资源,Preload才是你的朋友👬。

Preload另一个好处就是可以进行内容协商,而HTTP/2 Push不能。这意味着如果你想使用Client-Hints来决定发给服务器正确的图片,或者是Accept首页来决定最佳的格式,HTTP/2 Push会爱莫能助。

所以…

我希望你现在确信,Preload开辟了一套以前并不可行的加载功能,欢迎去Chrome Canary中使用它~

 

扩展阅读

参与评论