quicklink源码解析

前一阵子在逛github时看到一个GoogleChromeLabs推出的缓存工具库quicklink,介绍上说它是Faster subsequent page-loads by prefetching in-viewport links during idle time。也就是说可以提前缓存在接下来要访问的页面资源。此前了解过<link rel="prefetch" href="xxx" as="xxx">也可以做这个事,这俩是什么关系呢?README上说到其实底层就是用的prefetch,只不过由于兼容性问题,会提供降级方案。所以这个库是将这些东西封装起来了,然后暴露出更易用的api。本文主要是解析它的代码实现。

预缓存其实涉及到两个核心问题:

  • 缓存策略:缓存哪些资源,即如何知道哪些资源需要预缓存
  • 缓存方式:具体通过什么方式来预缓存

缓存策略

quicklink给出的默认实现是缓存那些在视口中的链接,也提供了api来手动指定缓存哪些资源。

首先第一个问题是:如何知道哪些链接在视口当中?一种方式是监听scoll事件,判断每个链接在视口中的坐标。缺点很明显,scroll触发很频繁需要截流操作,另外判断坐标的方式会触发重排重绘。quicklink使用的是IntersectionObserver,它提供了一种异步的方式来监听目标元素是否进入了视口或指定祖先元素。google developers上有一篇文章介绍了如何以及哪里使用它,例如图片懒加载。

看看quicklink中的实现方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
// isIntersecting: entry是否进入了视口
if (entry.isIntersecting) {
const link = entry.target;
// ...
prefetcher(link.href); // 预缓存指定链接
}
});
});

// options.el: 监听哪个元素下面的链接
Array.from((options.el || document).querySelectorAll('a'), link => {
observer.observe(link);
});

唯一的缺点是IntersectionObserver兼容性不大好:

兼容性

为此需要一个polyfill

另外为了不影响主线程,将兼容逻辑放到了requestIdleCallback中,只在浏览器空闲时执行observe。 如果不支持requestIdleCallback会回退到setTimeout的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
};

以上就是quicklink的缓存策略,Guess.js 做的更多,使用统计学和机器学习来基于用户行为预测预缓存资源。

缓存方式

上面也说到会优先使用prefetch, 降级方案是XHR,关于二者的区别在这篇博客中有提到:

xhr与prefetch区别

如何判断浏览器是否支持prefetch,主要是借助link.relList.supports

1
2
3
4
5
6
function support(feature) {
const link = document.createElement('link');
return link.relList && link.relList.supports && link.relList.supports(feature);
}

support('prefetch'); // true

使用prefetch其实挺简单,创建link标签设置适当的属性即可:

1
2
3
4
5
6
7
8
9
10
11
12
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);
});
}

注意:因为quicklink无法知道目标url是什么类型,所以此处没有办法设置linkas属性。

降级的XHR方案同样简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
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();
});
}

这里没有考虑status30x的情况,不知道是出于何种原因。