【PWA学习与实践】(10)使用Resource Hint提高页面加载性能与体验

《PWA学习与实践》系列文章已整理至gitbook - PWA学习手册,文字内容已同步至learning-pwa-ebook。转载请注明做者与出处。javascript

本文是《PWA学习与实践》系列的第十篇文章。也许你尚未据说过或不了解Resource Hint,可是经过本文,你会快速学习到这一件页面加载性能利器。本系列相关demo的代码均可以在github repo中找到。html

PWA做为时下最火热的技术概念之一,对提高Web应用的安全、性能和体验有着很大的意义,很是值得咱们去了解与学习。对PWA感兴趣的朋友欢迎关注《PWA学习与实践》系列文章。前端


对以前的文章感兴趣的话,能够从这里找到:java


引言

咱们知道,在没有缓存的状况下,不管是HTML、javascript仍是一些API数据,页面的每个请求都须要从客户端发起后经由服务端返回。在这种状况下,咱们每一次涉及远程请求的交互(打开一个页面、查询列表数据、动态加载js脚本等)都会有网络延迟。若是咱们可以预测或指定页面预先进行一些网络操做,例如DNS解析或者预加载资源,那么当咱们在以后的操做中涉及到这部分资源时,加载会更迅速,交互也会更加流畅。git

固然,目前已经有一些技术手段来帮助咱们实现资源的预加载,例如常见的使用XMLHttpRequest来获取资源并进行缓存。然而,这些技术都是应用层面的,并不是Web标准,某些需求也没法准确实现。同时,在性能方面也存在着问题。好在目前已有相关的Web标准(Resource Hint)涉及到这一部分,经过它,能够在浏览器原生层面实现这些功能,同时提供性能保证。下面咱们来了解一下Resource Hint相关技术。github

1. Resource Hint

Resource Hint是一系列相关标准,来告诉浏览器哪些源(origin)下的资源咱们的Web App想要获取,哪些资源在以后的操做或浏览时须要被使用,以便让浏览器可以进行一些预先链接或预先加载等操做。Resource Hint有DNS Prefetch、Preconnect、Prefetch和Prerender这四种。web

1.1. DNS Prefetch

当咱们在注重前端性能优化时,可能会忽略了DNS解析。然而DNS的解析也是有耗时的。在Chrome的Timing Breakdown Phase中,第三阶段就是DNS查询。DNS Prefetch就是帮助咱们告知浏览器,某个源下的资源在以后会要被获取,这样浏览器就会(Should)尽早解析它。chrome

Resource Hint主要经过使用link标签。rel属性肯定类型,href属性则指定相应的源或资源URL。DNS Prefetch能够像下面这样使用:跨域

<link rel="dns-prefetch" href="//yourwebsite.com">
复制代码

1.2. Preconnect

咱们知道,创建链接不只须要DNS查询,还须要进行TCP协议握手,有些还会有TLS/SSL协议,这些都会致使链接的耗时。所以,使用Preconnect能够帮助你告诉浏览器:“我有一些资源会用到某个源,能够帮我预先创建链接。”浏览器

根据规范,当你使用Preconnect时,浏览器大体作了以下处理:

  • 首先,解析Preconnect的URL
  • 其次,根据当前link元素中的属性进行cors的设置
  • 默认先将credential设为true;若是cors为Anonymous而且存在跨域,则将credential置为false
  • 最后进行链接

使用Preconnect只须要将rel属性设为preconnect便可:

<link rel="preconnect" href="//yourwebsite.com">
复制代码

固然,你也能够设置CORS

<link rel="preconnect" href="//yourwebsite.com" crossorigin>
复制代码

须要注意的是,标准并无硬性规定浏览器必定要(而是SHOULD)完成整个链接过程,浏览器能够视状况完成部分工做。

1.3. Prefetch

你能够把Prefetch理解为资源预获取。通常来讲,能够用Prefetch来指定在紧接着以后的操做或浏览中须要使用到的资源,让浏览器提早获取。因为仅仅是提早获取资源,所以浏览器不会对资源进行预处理,而且像CSS样式表、JavaScript脚本这样的资源是不会自动执行并应用于当前文档的。

须要注意的是,和DNS Prefetch、Preconnect使用不太同样的地方是,Prefetch有一个as的可选属性,用来指定获取资源的类型。因为不一样的资源类型会具备不一样的优先级、CSP、请求头等,所以该属性很重要。下表列出了一些经常使用资源的as属性值:

资源使用者 写法
<audio> <link rel=preload as=audio href=...>
<video> <link rel=preload as=video href=...>
<track> <link rel=preload as=track href=...>
<script>, Worker's importScripts <link rel=preload as=script href=...>
<link rel=stylesheet>, CSS @import <link rel=preload as=style href=...>
CSS @font-face <link rel=preload as=font href=...>
<img>, <picture>, srcset, imageset <link rel=preload as=image href=...>
SVG's <image>, CSS *-image <link rel=preload as=image href=...>
XHR, fetch <link rel=preload as=fetch crossorigin href=...>
Worker, SharedWorker <link rel=preload as=worker href=...>
<embed> <link rel=preload as=embed href=...>
<object> <link rel=preload as=object href=...>
<iframe>, <frame> <link rel=preload as=document href=...>
HTML <link rel=preload as=html href=...>

能够看到,Prefetch的可选资源类型很是丰富,除了咱们经常使用的scriptstyle,甚至还包括XHR、video、img等,基本涵盖了Web中的各种资源。为了解决Prefetch中某些资源(例如XHR)的跨域问题,能够为其应用CORS属性。一个基本的Prefetch写法也很简单:

<link rel="prefetch" href="/my.little.script.js" as="script">
复制代码

1.4. Prerender

上一部分咱们讲了Prefetch,而Prerender则是Prefetch的更进一步。能够粗略地理解为“预处理”(预执行)。

经过Prerender“预处理”的资源,浏览器都会做为HTML进行处理。浏览器除了会去获取资源,还可能会预处理(MAY preprocess)该资源,而该HTML页面依赖的其余资源,像<script><style>等页面所需资源也可能会被处理。可是预处理会因为浏览器或当前机器、网络状况的不一样而被不一样程度地推迟。例如,会根据CPU、GPU和内存的使用状况,以及请求操做的幂等性而选择不一样的策略或阻止该操做。

注意,因为这些预处理操做的不可控性,当你只是须要可以预先获取部分资源来加速后续可能出现的网络请求时,建议使用Prefetch。当使用Prerender时,为了保证兼容性,目标页面能够监听visibilitychange事件并使用document.visibilityState来判断页面状态。

When prerendering a document the user agent MUST set the document's visibilityState value to prerender. —— W3C Working Draft

Prerender的使用方式很是简单,与DNS Prefetch和Preconnect相似,指定rel属性为prerender

<link rel="prerender" href="//yourwebsite.com/nextpage.html">
复制代码

2. Resource Hint的具体使用方式

在上面的部分里,我主要介绍了DNS Prefetch、Preconnect、Prefetch和Prerender这四种RHL(Resource Hint Link),而且简单介绍了如何在link中使用它们。然而除了直接在HTML中加入对应link标签外,还能够经过其余几种方式触发浏览器的Resource Hint。为了更加直观,下面咱们仍是以图书搜索这个demo为例来看看能够经过哪些方法来使用Resource Hint。

假设已经为该demo添加详情页nextpage.html及其依赖的nextpage.js,当点击列表中的图书时会进行跳转。

2.1. 文档head中的link元素

这是Resource Hint最经常使用的一种方式,咱们上面介绍的各类示例也就是使用的这种方式。例如想要指定Prefetch nextpage.js脚本能够这么写:

<link rel="prefetch" href="./nextpage.js" as="script">
复制代码

2.2. HTTP Link头字段

能够经过Link HTTP header来使用Resource Hint。Link HTTP header和link元素是等价的。

The Link entity-header field provides a means for serialising one or more links in HTTP headers. It is semantically equivalent to the element in HTML, as well as the atom:link feed-level element in Atom. —— RFC5988

Link主要由两部分组成——URI-Referencelink-paramURI-Reference至关于link元素中的href属性;link-param则包括了reltitletype等一系列元素属性,使用;分割。所以能够在响应头中添加如下部分:

Link: </nextpage.js>; rel="prefetch"; as="script"
复制代码

咱们的demo使用了koa-static这个中间件,只要作以下修改便可:

// app.js
app.use(serve(__dirname + '/public', {
    maxage: 1000 * 60 * 60,
    setHeaders: (res, path, stats) => {
        if (/index.html/.test(path)) {
            res.setHeader('Link', '</nextpage.js>; rel="prefetch"; as="script"');
        }
    }
}));
复制代码

你会发现,在访问index.html时,浏览器就会向服务器请求nextpage.js这个页面自己并“不须要”用到的资源。

2.3. 向文档动态添加link元素

link元素也支持咱们经过js动态向文档添加。对于动态添加的RHL,浏览器也会对其应用Resource Hint策略。添加link的方式和添加普通dom元素一致。

var hint = document.createElement('link');
hint.rel = 'prefetch';
hint.as = 'script';
hint.href = '/nextpage.js';
document.head.appendChild(hint);
复制代码

2.4. 改变已有link元素的href属性

当你改变页面中原有RHL的href属性(或者prefetch时的as属性)时,会当即触发对新资源的Resource Hint。例如在以下代码执行后

var hint = document.querySelector('[rel="prefetch"]');
hint.href = './the.other.nextpage.js';
复制代码

浏览器至关于接收到了新的Resource Hint“指示”,并在合适的时机向服务端请求the.other.nextpage.js这个资源。注意,当你修改as属性时,也会触发Resource Hint。

注意,若是你想经过修改已有link元素预获取nextpage.html这个资源,而后像下面这样写会触发两次请求。

var hint = document.querySelector('[rel="prefetch"]');
hint.as = 'html'; // 触发第一次请求,再次请求./nextpage.js
hint.href = './nextpage.html'; // 请求./nextpage.html
复制代码

2. Preload

既然提到了Resource Hint,那么不得不介绍一下与其相似的Preload。在遇到须要Preload的资源时,浏览器会 马上 进行预获取,并将结果放在内存中,资源的获取不会影响页面parse与load事件的触发。直到再次遇到该资源的使用标签时,才会执行。

(Preload) Initiating an early fetch and separating fetching from resource execution.

例以下面这个HTML片断:

<head>
    <link rel="preload" href="./nextpage.js" as="script">
    <script type="text/javascript" src="./current.js"></script>
    <script type="text/javascript" src="./nextpage.js"></script>
<head>
复制代码

浏览器首先会去获取nextpage.js,而后获取并执行current.js,最后,遇到使用nextpage.js资源的script标签时,将已经获取的nextpage.js执行。因为咱们会将script标签置于body底部来保证性能,所以能够考虑在head标签中添加这些资源的Preload来加速页面的加载与渲染。

更进一步,咱们还能够监听Preload的状况,并触发自定以操做

<script> function preloadFinished(e) { ... } function preloadError(e) { ... } </script>
<!-- listen for load and error events -->
<link rel="preload" href="app.js" as="script" onload="preloadFinished()" onerror="preloadError()">
复制代码

正如在引言中所提到的,在过去若是咱们想预加载一些资源都会用一些应用层面的技术手段,但每每会遇到两个问题:

  • 咱们须要先获取资源,而后在适当时执行,但二者并不易于分离
  • 不管哪一种技术实现,都会带来必定的性能与体验损伤

Preload(包括前文提到的Prefetch等RHL)给咱们带来的价值就是从浏览器层面很好地将资源的加载与执行分离了,并在浏览器层面来保证良好的性能体验。

看到这里,也许你会疑惑,都是会预获取资源,都是资源的获取与执行分离,那么Preload与Prefetch有什么区别呢?

这是它最容易与Prefetch混淆的地方。在标准里有这么一段话解释二者区别:

The application can use the preload keyword to initiate early, high-priority, and non-render-blocking fetch of a CSS resource that can then be applied by the application at appropriate time

与Prefetch相比,Preload会强制浏览器当即获取资源,而且该请求具备较高的优先级(mandatory and high-priority),所以建议对一些当前页面会立刻用到资源使用Preload;相对的,Prefetch的资源获取则是可选与较低优先级的,其是否获取彻底取决于浏览器的决定,适用于预获取未来可能会用到的资源。

为了节省没必要要的带宽消耗,若是Preload的资源在3s内没有被使用,Chrome控制台会出现相似下图的警告。这时你就须要仔细思考,该资源是否有必要Preload了。

更多Preload与Prefetch的细节差别能够看这里 —— Preload, Prefetch And Priorities in Chrome

3. 写在最后

本文介绍了如何使用Resource Hint(以及Preload)来提高页面加载性能与体验,简单来讲:

  • DNS Prefetch 能够帮助咱们进行DNS预查询;
  • Preconnect 能够帮助咱们进行预链接,例如在一些重定向技术中,可让浏览器和最终目标源更早创建链接;
  • Prefetch 能够帮助咱们预先获取所需资源(而且不用担忧该资源会被执行),例如咱们能够根据用户行为猜想其下一步操做,而后动态预获取所需资源;
  • Prerender 则会更进一步,不只获取资源,还会预加载(执行)部分资源,所以若是咱们Prerender下一个页面,打开该页面时会让用户感受很是流畅;
  • Preload 则像是 Prefetch的升级版,会强制当即高优获取资源,很是适合Preload(尽早获取)一些关键渲染路径中的资源。

虽然,大部分PWA相关资料中并不会说起Resource Hint,可是正如我在第一篇文章中提到的

PWA自己实际上是一个概念集合,它不是指某一项技术,而是经过一系列的Web技术与Web标准来优化Web App的安全、性能和体验。

Resource Hint显然符合这一点。

咱们不该该将PWA局限在Service Worker离线缓存、提醒通知这些常见的PWA内容中,但愿读者也能开阔思惟,理解PWA背后的概念与思想。所以,在后续文章中我也会介绍前端存储(sessionStorage/localStorage/indexDB)、HTTP/2.0以及PWA进展等相关内容。

在下一篇里,咱们会一块儿来学习Google开源的PWA离线工具集 —— workbox。经过workbox,咱们能够学习各种离线策略,而且了解一些生产环境中须要考虑的问题。部分开源PWA解决方案也是基于workbox进行封装的。

《PWA学习与实践》系列

参考资料

相关文章
相关标签/搜索