近来,GoogleChromeLabs 推出了 quicklink,用以实现连接资源的预加载(prefetch)。本文在介绍其实现思路的基础上,会进一步探讨在预加载方面前端工程师还能够作什么。前端
quicklink 是一个经过预加载资源来提高后续方案速度的轻量级工具库。旨在提高浏览过程当中,用户访问后续页面时的加载速度。git
当咱们提到性能优化,每每都会着眼于对当前用户访问的这个页面,如何经过压缩资源大小、删减没必要要资源、加快页面解析渲染等方式提高用户的访问速度;而 quicklink 用了另外一种思路:我预先帮你加载(获取)你接下来最可能要用的资源,这样以后的真正使用到该资源(连接)时就会感受很是顺畅。github
照着这个思路,咱们须要解决的问题就是如何预先帮用户加载资源呢?这里其实涉及到两个问题:web
下面就结合 quicklink 源码来看看如何解决这两个问题。浏览器
注:下文提到的“预加载”/“预获取”均指 prefetch
首先要解决的是,经过什么方式来实现资源的预加载。即预加载的方式。缓存
咱们这里的预加载对应的英文是 prefetch。提到 prefetch 天然会想到使用浏览器的 Resource Hints,经过提示浏览器作一些“预操做”(例如 DNS 解析、资源下载等)来加快后续的访问。性能优化
若是对 prefetch 与 Resource Hints 不熟悉,能够看看这篇 《使用Resource Hint提高页面加载性能与体验》。
只须要下面这样一行代码就能够实现浏览器的资源预加载。是否是很是美妙?网络
<link rel="prefetch" href="/my.little.script.js" as="script">
所以,要预加载一个资源能够经过下面四行代码:前端工程师
const link = document.createElement(`link`); link.rel = `prefetch`; link.href = url; document.head.appendChild(link);
然而,咱们不得不面对兼容性的问题,在低版本 IE 与移动端是重灾区。app
好梦破灭。既然如此,咱们就须要一个相似 prefetch shim 的方式:在不支持 Resource Hints 的浏览器中,使用其余方式来预加载资源。对此,咱们能够利用浏览器自身的缓存策略,“实实在在”预先请求这个资源,这也造成了一种资源的“预获取”。而这最方便的就是经过 XHR:
const req = new XMLHttpRequest(); req.open(`GET`, url, req.withCredentials=true); req.send();
这样 shim 也完成了。最后,如何检测浏览器是否支持 prefetch 呢?
咱们能够经过 link
元素上 relList 属性的 support
方法来检查对 prefetch 的支持状况:
const link = document.createElement('link'); link.relList || {}).supports && link.relList.supports('prefetch');
结合这三个段代码,就造成了一个简易的 prefetcher:判断是否支持 Resource Hints 中的 prefetch,支持则使用它,不然回退使用 XHR 加载。
值得一提的是,使用 Resource Hints 与使用 XHR 来预加载资源仍是有一些重要差别的。草案中也提到了一些(主要是与性能以及与浏览器其余行为之间的冲突)。其中还有一点就是,Resource Hints 中的 prefetch 是否执行,彻底是由浏览器决定的,草案里有句话很是明显 —— the user agent SHOULD fetch。所以,全部 prefetch 的资源并不必定会真正被 prefetch。相较之下,XHR 的方式“成功率”则更高。这点在 Netflix 实施的性能优化案例中也提到了。
题外话:quicklink 中使用 fetch API 实现高优先级资源的加载。这是由于浏览器中会为全部的请求都设置一个优先级,高优请求会被优先执行;目前,
fetch
在 Chrome 中属于高优先级,在 Safari 中属于中等优先级。
有了资源预加载的方式,那么接下来就须要一个预加载的策略了。
这实际上是个见仁见智的问题。例如直接给你一个连接 https://my.test.com/somelink
,在没有任何背景信息的状况下,恐怕你彻底不知道是否须要预加载它。那对于这个问题,quicklink 是怎么解决的呢?或者说,quicklink 是经过什么策略来进行预加载的呢?
quicklink 用了一个比较直观的策略:只对处于视口内的资源进行预加载。这一点也比较好理解,网络上大多的资源加载、页面跳转都伴随着用户点击这类行为,而它要是不在你的视野内,你也就无从点击了。这必定程度上算是个必要条件。
这么一来,咱们所要解决的问题就是,若是判断一个连接是否处于可视区域内?
之前,对于这种问题,咱们作的就是监听 scroll
事件,而后判断某元素的位置,从而来“得知”元素是否进入了视区。传统的图片懒加载库 lazysize 等也是用这种策略。
document.addEventListener('scroll', function () { // ……判断元素位置 });
注:目前 lazysize 也有了基于 IntersectionObserver 的实现
固然,须要特别注意滚动监听的性能,例如使用截流、避免强制同步布局、 passive: true
等方式缓解性能问题。
不过如今咱们有了一个新的方式来实现这一功能 —— IntersectionObserver
:
const observer = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) { const link = entry.target; // 预加载连接 } }); }); // 对全部 a 标签添加观察者 Array.from(options.el.querySelectorAll('a'), link => { observer.observe(link); });
IntersectionObserver
会建立一个观察者,专门用来观察与通知元素进出视口的状况。如上述代码所示,IntersectionObserver
能够观察全部 a
元素的位置状况(主要是进入视野)。
对IntersectionObserver
不了解的同窗能够参考 Google 的IntersectionObserver
介绍文章。
可是以下图所示, IntersectionObserver
存在兼容性问题,所以要在不兼容的浏览器中使用 quicklink,会须要一个 polyfill。
目前,咱们已经把 quicklink 的两大部分(预加载的方式和预加载的策略)的原理和简单实现讲完了。整个 quicklink 很是简洁,这些基本就是 quicklink 的核心。剩下的就是一些参数检查、额外的规则特性等。
题外话:为了进一步保证性能,quicklink 使用requestIdleCallback
在空闲时间查询页面a
标签并挂载观察者。对requestIdleCallback
不了解的同窗能够看看 Google 的这篇文章。
到这里,quicklink 的实现就基本讲完了。仔细回想一下,quicklink 其实提供了咱们一种经过“预加载”来实现性能优化的思路(粗略来讲像是用流量换体验)。这种方式我在前面也提到了,其实能够分为两个部分:
其实两部分彷佛都有能够做为的地方。例如如何保证 prefetcher(资源预加载器)的成功率能更高,以及目前使用的回退方案 XHR 其实在预加载没法缓存的资源时所受的限制等。
此外,咱们在这里还能够来聊一聊策略这块。
因为 quicklink 是一个业务无关的轻量级功能库,因此它采用了一个简单但必定程度上有效的策略:预加载视野内的连接资源。然而在实际生产中,咱们面对的是更复杂的环境,更复杂的业务,反而会须要更精准的预加载判断。所以,咱们彻底能够从 quicklink 中剥离出 prefetcher 来做为一个预加载器;而在策略部分使用本身的实现,例如:
上面这些场景只是抛砖引玉,相信你们还会有更多更好的场景能够来助力咱们的前端应用“起飞”。此外,咱们彻底能够借助一些构建工具、数据采集与分析平台来实现策略的自动提取与注入,优化整个预加载的流程。
预加载、Resource Hints等由来已久。quicklink 经过提出了一种可行的方案让它又进入了你们的视野,给咱们展示了性能优化的另外一面。但愿你们经过了解 quicklink 的实现,也能有本身的想法与启发。
相信随着浏览器的不断进化,标准的不断前行,前端工程师对极致体验与性能要求的不断提升,咱们的产品将会愈来愈好。