最近Google Chrome lab的一个开源项目quicklink很火,号称可以极大提升页面的加载速度,社区中也有很多使用该项目来做页面加载优化的尝试,但quicklink项目本身的实现其实极为简洁,源码总共不过百行而已,其思路也不复杂,就是通过在浏览器空闲阶段预加载view-port内的外部资源链接来提升页面加载速度,总体来说并没有什么奇技淫巧。而我这里主要想记录和分享的是我在阅读quicklink源码中头脑发散的所思所得。
一,关于requestIdleCallback
首先我们来看下quicklink的源码结构
| src
| - index.mjs
| - prefetch.mjs
| - request-idle-callback.mjs
index.mjs为项目的入口文件,prefetch.mjs负责外部资源预加载的实现,request-idle-callback.mjs负责检测浏览器是否处于空闲状态,该模块的源码如下:
const requestIdleCallback = requestIdleCallback ||
function (cb) {
const start = Date.now();
return setTimeout(function () {
cb({
didTimeout: false,
timeRemaining: function () {
return Math.max(0, 50 - (Date.now() - start));
},
});
}, 1);
};
export default requestIdleCallback;
requestIdleCallback是Chrome浏览器 47版本之后提供的原生的性能优化API,用于在浏览器空闲时执行指定的回调函数,这段代码是对requestIdleCallback的兼容性实现,在一些没有提供该API的浏览器中利用后面的自定义shim函数实现了对该功能的模拟。在阅读这段代码时我的主要疑问是后面这段平平无奇利用setTimeout处理的函数为什么就能达到requestIdleCallback的效果。于是乎我搜索了一下google关于web开发的博文,终于找到了相关文档,文档皆为英文,其大意是:
- requestIdleCallback接受一个回调函数作为入参
- 该回调函数会接受一个名为deadline的object作为入参
- dealline包含一个timeRemaining的函数,该函数的返回值表示当前任务的剩余时间,当该值为0时,就可以继续规划其他任务了。
- 当回调被执行之后timeRemaining函数会返回0,deadline的didTimeout属性值会为true
具体代码示例如下:
requestIdleCallback(myNonEssentialWork);
function myNonEssentialWork (deadline) {
while (deadline.timeRemaining() > 0 && tasks.length > 0)
doWorkIfNeeded();
if (tasks.length > 0)
requestIdleCallback(myNonEssentialWork);
}
requestIdleCallback与requestAnimationFrame相同的是他们都接受一个回调函数作为入参,回调函数的执行时机由浏览器来决定,不同的是requestAnimationFrame在浏览器每帧绘制时都会执行,而requestIdleCallback必须是在浏览器线程空闲时才会执行,如果浏览器一直处于繁忙状态,就有可能导致requestIdleCallback指定的任务一直都不会执行,要解决这个问题可以给requestIdleCallback传递一个包含timeout属性的对象作为第二个入参,这样浏览器在达到该timeout设定的时间后一定会执行回调。示例代码如下:
requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });
requestIdleCallback的机制使其很适合用来做一些对页面来说不紧要的任务,比如说发送页面埋点数据之类。
二,关于prefetch与preload
首先来看下quicklink实现预加载的源码:
function linkPrefetchStrategy(url) {
return new Promise((resolve, reject) => {
const link = document.createElement(`link`);
link.rel = `prefetch`;
link.href = url;
link.onload = resolve;
link.onerror = reject;
document.head.appendChild(link);
});
};
function xhrPrefetchStrategy(url) {
return new Promise((resolve, reject) => {
const req = new XMLHttpRequest();
req.open(`GET`, url, req.withCredentials=true);
req.onload = () => {
(req.status === 200) ? resolve() : reject();
};
req.send();
});
}
function highPriFetchStrategy(url) {
return self.fetch == null
? xhrPrefetchStrategy(url)
: fetch(url, {credentials: `include`});
}
const supportedPrefetchStrategy = support('prefetch')
? linkPrefetchStrategy
: xhrPrefetchStrategy;
function prefetcher(url, isPriority, conn) {
if (preFetched[url]) {
return;
}
if (conn = navigator.connection) {
if ((conn.effectiveType || '').includes('2g') || conn.saveData) return;
}
return (isPriority ? highPriFetchStrategy : supportedPrefetchStrategy)(url).then(() => {
preFetched[url] = true;
});
};
export default prefetcher;
这段代码的执行逻辑示意图如下:
-
加载力度不同: preload会强制浏览器加载指定资源,同时不会阻塞document的onload事件;prefetch仅仅是提示浏览器这个资源将来可能需要,但至于是否要加载或者何时加载则由浏览器本身决定
-
应用场景不同: preload主要告知浏览器预先请求当前页面所必要的资源;prefetch则主要是针对将来的页面需要加载的资源。如果页面A使用prefetch发起一个页面B某个资源的请求,该请求与页面B的导航请求有可能会同步进行,而如果使用preload,那么当离开页面A时preload请求会立即中断。
-
网络优先级不同: preload的优先级要高于prefetch,而且preload不同as属性的资源网络优先级也不同,比如as='style'的优先级就比as="script"的优先级要高。
理解了这几点差异之后之前的疑惑就迎刃而解了,在浏览器中,preload相对于fetch API,prefetch和xhr都具有更高的优先级,且是强制加载,因此更适合用于处理高优先级的资源请求,对于非优先级的请求使用prefetch是比较合理的,相信quicklink未来也会更改函数highPriFetchStrategy的实现,用preload来完成。不管是prefetch还是preload,他们在完成资源加载后都不会执行,这一点也非常关键。
结语
quicklink是一个短小精悍的项目,虽然没有什么很高深的技术原理,但是仔细思考依然能发现一些比较有趣的小知识点,从点到面,不断发散才能更好的形成技术知识体系并活学活用,而不仅仅是一个知识点。比如requestIdleCallback的优化还可以怎么用,react v16版本的一个重大变更就是fiber的任务调度算法,在其中就利用了requestIdleCallback去执行低优先级的任务。