记录从quicklink源码中发散出来的知识点

1,347 阅读5分钟

最近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;

这段代码的执行逻辑示意图如下:

quicklink做预加载的思路是如果设置了优先级属性isPriority为true,就使用浏览器fetch API或者ajax的方式优先请求指定的URL资源,否则就判断浏览器是否原生支持prefetch特性,如果支持就通过创建link标签使用link tag的prefetch来加载资源。isPriority属性默认为false,通常来说不会去改变它,所以对于非优先级资源预加载的实现就主要依赖于link tag的prefetch特性,当我看到这里的时候我很好奇为什么是prefetch而不是preload,我们知道link标签的preload属性也可以做预加载,为什么quicklink不使用preload呢?这两者到底有什么区别?因此我开始了深挖之旅,终于在medium上找到了相关资源,preload, prefetch and priority in chrome,medium访问需要翻墙,且内容为英文,我查阅之后,概括为如下几点:

  • 加载力度不同: 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去执行低优先级的任务。