quicklink
通过在空闲时间预取或预渲染视口内的链接,实现后续页面加载更快
工作原理
Quicklink 试图使导航到后续页面的加载速度更快。它:
- 检测视口内的链接(使用Intersection Observer)
- 等待浏览器空闲(使用requestIdleCallback)
- 检查用户是否处于慢速连接(使用
navigator.connection.effectiveType
)或启用了数据保护(使用navigator.connection.saveData
) - 预取(使用
<link rel=prefetch>
或XHR)或预渲染(使用Speculation Rules API)链接的URL。提供一些对请求优先级的控制(如果支持,可以切换到fetch()
)。
为什么
这个项目旨在为网站提供一个即插即用的解决方案,根据用户视口中的内容预取或预渲染链接。它还致力于保持小巧(压缩并gzip后小于2KB)。
多页应用
安装
npm install quicklink
你也可以从unpkg.com/quicklink获取quicklink
。
使用
初始化后,quicklink
将在空闲时自动预取视口内链接的URL。
快速开始:
<!-- 从dist引入quicklink -->
<script src="dist/quicklink.umd.js"></script>
<!-- 初始化(你可以在任何时候进行) -->
<script>
quicklink.listen();
</script>
例如,你可以在load
事件触发后初始化:
<script>
window.addEventListener('load', () => {
quicklink.listen();
});
</script>
ES模块导入:
import {listen, prefetch} from 'quicklink';
单页应用(React)
安装
npm install quicklink webpack-route-manifest --save-dev
然后,按照这里的说明将Webpack路由清单配置到你的项目中。
这将生成一个名为rmanifest.json
的路由和代码块映射。可以在以下位置获取:
- URL:
site_url/rmanifest.json
- Window对象:
window.__rmanifest
使用
在你想添加预取功能的地方导入quicklink
React HOC。
用withQuicklink()
HOC包装你的路由。
例子:
import {withQuicklink} from 'quicklink/dist/react/hoc.js';
const options = {
origins: [],
};
<Suspense fallback={<div>加载中...</div>}>
<Route path='/' exact component={withQuicklink(Home, options)} />
<Route path='/blog' exact component={withQuicklink(Blog, options)} />
<Route path='/blog/:title' component={withQuicklink(Article, options)} />
<Route path='/about' exact component={withQuicklink(About, options)} />
</Suspense>;
API
quicklink.listen(options)
返回:Function
返回一个"重置"函数,该函数将清空活动的IntersectionObserver
和已预取或预渲染的URL缓存。这可以在页面导航之间和/或发生重大DOM更改时使用。
options.prerender
- 类型:
Boolean
- 默认值:
false
是否从默认的预取模式切换到视口内链接的预渲染模式。
**注意:**当浏览器不支持预渲染时,预渲染模式(当此选项设置为true时)将回退到预取模式。
options.prerenderAndPrefetch
- 类型:
Boolean
- 默认值:
false
是否同时激活预取和预渲染模式。
options.delay
- 类型:
Number
- 默认值:
0
每个链接在被预取之前需要停留在视口内的时间,以毫秒为单位。
options.el
- 类型:
HTMLElement|NodeList<A>
- 默认值:
document.body
要观察的DOM元素,用于检测视口内需要预取的链接,或锚点元素的NodeList。
options.limit
- 类型:
Number
- 默认值:
Infinity
在观察options.el
容器时可以预取或预渲染的总请求数。
options.threshold
- 类型:
Number
- 默认值:
0
每个链接必须进入视口的面积百分比才能被获取,以小数形式表示(例如,0.25 = 25%)。
options.throttle
- 类型:
Number
- 默认值:
Infinity
在观察options.el
容器时,同时进行的请求数限制。
options.timeout
- 类型:
Number
- 默认值:
2000
requestIdleCallback
超时时间,以毫秒为单位。
**注意:**浏览器必须在配置的持续时间内保持空闲状态才会进行预取。
options.timeoutFn
- 类型:
Function
- 默认值:
requestIdleCallback
用于指定timeout
延迟的函数。
这可以替换为自定义函数,如networkIdleCallback(参见演示)。
默认情况下,使用requestIdleCallback
或嵌入的polyfill。
options.priority
- 类型:
Boolean
- 默认值:
false
options.el
容器内的URL是否应被视为高优先级。
当设置为true
时,如果支持,quicklink将尝试使用fetch()
API(而不是link[rel=prefetch]
)。
options.origins
- 类型:
Array<String>
- 默认值:
[location.hostname]
允许预取的URL主机名的静态数组。
默认为相同的域名源,这可以防止任何跨域请求。
重要:空数组([]
)允许预取所有来源。
options.ignores
- 类型:
RegExp
或Function
或Array
- 默认值:
[]
确定是否应预取URL。
当RegExp
测试为正,Function
返回true
,或Array
包含字符串时,则不会预取该URL。
注意:
Array
可以包含String
、RegExp
或Function
值。
**重要:**此逻辑在源匹配之后执行!
options.onError
- 类型:
Function
- 默认值:无
一个可选的错误处理函数,用于接收预取请求中的任何错误。
默认情况下,这些错误会被静默忽略。
options.hrefFn
- 类型:
Function
- 默认值:无
一个可选函数,用于生成要预取的URL。它接收一个Element作为参数。
quicklink.prefetch(urls, isPriority)
返回:Promise
提供的urls
始终通过Promise.all
传递,这意味着结果将始终解析为一个数组。
**重要:**你必须自行
catch
请求错误。
urls
- 类型:
String
或Array<String>
- 必需:
true
一个或多个要预取的URL。
**注意:**每个
url
值都是相对于当前位置解析的。
isPriority
- 类型:
Boolean
- 默认值:
false
是否将URL视为"高优先级"目标。
默认情况下,对prefetch()
的调用为低优先级。
**注意:**这与
listen()
的priority
选项行为相同。
quicklink.prerender(urls)
返回:Promise
**重要:**你必须自行
catch
请求错误。
urls
- 类型:
String
或Array<String>
- 必需:
true
一个或多个要预渲染的URL。
**注意:**推测性规则API支持同站跨域预渲染,需要选择加入头部。
填充
quicklink
:
- 包含一个非常小的requestIdleCallback回退
- 需要支持
IntersectionObserver
(参见Can I Use)。我们建议使用Polyfill.io等服务有条件地填充此功能:
<script src="https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver"></script>
或者,参见Intersection Observer polyfill。
使用示例
为预取资源设置自定义超时
默认为2秒(通过requestIdleCallback
)。这里我们将其覆盖为4秒:
quicklink.listen({
timeout: 4000,
});
设置特定的锚元素NodeList以观察视口内的链接
默认为document
。
quicklink.listen({
el: document.querySelectorAll('a.linksToPrefetch'),
});
设置DOM元素以观察视口内的链接
默认为document
。
quicklink.listen({
el: document.getElementById('carousel'),
});
以编程方式prefetch()
URL
如果你更喜欢提供一个静态的URL列表进行预取,而不是检测视口内的URL,支持自定义URL。
// 单个URL
quicklink.prefetch('2.html');
// 多个URL
quicklink.prefetch(['2.html', '3.html', '4.js']);
// 多个URL,高优先级
// 注意:也可用于单个URL!
quicklink.prefetch(['2.html', '3.html', '4.js'], true);
以编程方式prerender()
URL
如果你更喜欢提供一个静态的URL列表进行预渲染,而不是检测视口内的URL,支持自定义URL。
// 单个URL
quicklink.prerender('2.html');
// 多个URL
quicklink.prerender(['2.html', '3.html', '4.js']);
设置滚动时预取的请求优先级
默认为低优先级(rel=prefetch
或XHR)。对于高优先级(priority: true
),尝试使用fetch()
或回退到XHR。
**注意:**这会在
options.el
容器内找到的URL上运行prefetch(..., true)
。
quicklink.listen({priority: true});
指定允许的源的自定义列表
提供应该可预取的主机名列表。默认情况下只允许相同源。
**重要:**你还必须包括你自己的主机名!
quicklink.listen({
origins: [
// 添加自己的
'my-website.com',
'api.my-website.com',
// 添加第三方
'other-website.com',
'example.com',
// ...
],
});
允许所有源
启用所有跨源请求。
quicklink.listen({
origins: true,
// 或
origins: [],
});
自定义忽略模式
这些过滤器在origins
匹配之后运行。忽略可用于避免大文件下载或动态响应DOM属性。
// 默认启用同源限制。
//
// 此示例将忽略所有对以下内容的请求:
// - 所有"/api/*"路径名
// - 所有".zip"扩展名
// - 所有具有"noprefetch"属性的<a>标签
//
quicklink.listen({
ignores: [
/\/api\/?/,
uri => uri.includes('.zip'),
(uri, elem) => elem.hasAttribute('noprefetch'),
],
});
你可能还希望忽略包含URL片段的URL预取(例如index.html#top
)。如果你(1)正在使用页面中的锚点标题或(2)为单页应用设置了URL片段,并希望避免为类似的URL触发预取,这可能很有用。
使用ignores
可以实现如下:
quicklink.listen({
ignores: [
uri => uri.includes('#'),
// 或正则表达式:/#(.+)/
// 或元素匹配:(uri, elem) => !!elem.hash
],
});
通过hrefFn回调自定义要预取的URL
hrefFn方法允许动态构建要预取的URL(例如API端点),而不是预取href
属性URL。
quicklink.listen({
hrefFn(element) {
return element.href.replace('html', 'json');
},
});
浏览器支持
quicklink
提供的预取可以视为渐进增强。跨浏览器支持如下:
- 无需填充:Chrome、Safari ≥ 12.1、Firefox、Edge、Opera、Android Browser、Samsung Internet。
- 使用Intersection Observer polyfill(约6KB gzip压缩/最小化):Safari ≤ 12.0、IE11
- 使用上述填充以及Set()和Array.from填充:IE9和IE10。Core.js提供了
Set()
和Array.from()
两种垫片。es6-shim等项目是你可以考虑的替代方案。
某些功能具有分层支持:
- Network Information API,用于检查用户是否有慢速有效连接类型(通过
navigator.connection.effectiveType
),仅在Chrome 61+和Opera 57+中可用 - 如果选择
{priority: true}
且Fetch API不可用,将使用XHR代替。
直接使用预取器
prefetch
方法可以单独导入以在其他项目中使用。
该方法包含了尊重数据节省模式和 2G 连接的逻辑。它还会根据 isPriority
值和当前浏览器的支持情况,通过 fetch()
、XHR 或 link[rel=prefetch]
发出请求。
在将 quicklink
安装为依赖项后,你可以按以下方式使用它:
<script type="module">
import {prefetch} from 'quicklink';
prefetch(['1.html', '2.html']).catch(error => {
// 处理自己的错误
});
</script>
演示
Glitch 演示
- 在多页面网站中使用 Quicklink
- 通过 Workbox 将 Quicklink 与 Service Workers 结合使用
- 使用 Quicklink 预取 API 调用而非
href
属性 - 使用 Quicklink 预渲染特定页面
研究
这是我们演示的 WebPageTest 运行结果,通过 quicklink 的预取功能将页面加载性能提高了最多 4 秒。YouTube 上有一个预取前后对比的视频。
出于演示目的,我们在 Firebase 托管上部署了 Google Blog 的一个版本。然后我们部署了另一个版本,在主页上添加了 quicklink,并对从主页导航到自动预取的文章进行了基准测试。预取版本加载得更快。
请注意:这绝不是对视口内链接预取的优缺点的全面基准测试。只是展示了这种方法可能带来的潜在改进。你自己的实际效果可能会有很大差异。
其他说明
会话拼接
跨源预取(例如 a.com/foo.html
预取 b.com/bar.html
)有许多限制。其中一个限制是会话拼接。b.com
可能期望 a.com
的导航请求包含会话信息(例如临时 ID - 如 b.com/bar.html?hash=<>×tamp=<>
),这些信息用于自定义体验或记录分析信息。如果会话拼接需要 URL 中的时间戳,预取并存储在 HTTP 缓存中的内容可能与用户最终导航到的内容不同。这带来了一个挑战,因为它可能导致双重预取。
为了解决这个问题,你可以考虑通过 ping 属性(单独)传递会话信息,以便源站可以异步拼接会话。
广告相关考虑
依赖广告作为收入来源的网站不应预取广告链接,以避免无意中计算这些广告位置的点击次数,这可能导致广告点击率(CTR)虚高。
广告主要以两种方式出现在网站上:
-
在 iframe 内: 默认情况下,大多数广告服务器在 iframe 内渲染广告。在这些情况下,除非开发人员明确传入广告 iframe 的 URL,否则 Quicklink 不会预取这些广告链接。原因是库对视口内元素的查找仅限于顶级源的元素。
-
在 iframe 外: 当网站显示同源广告,直接显示在顶级文档中(例如,自行托管广告并直接在页面中显示)时,开发人员需要明确告诉 Quicklink 避免预取这些链接。这可以通过将广告链接的 URL 或子路径,或包含它的元素传递给自定义忽略模式列表来实现。
相关项目
- 使用 Gatsby?你已经免费获得了大部分这些功能。它使用
Intersection Observer
预取所有在视图中的链接,并为本项目提供了重要灵感。 - 想要更加数据驱动的方法?看看 Guess.js。它使用分析和机器学习基于用户如何浏览你的网站来预取资源。它还有 Webpack 和 Gatsby 的插件。
- WordPress 用户现在可以从插件库中获得 quicklink 作为 WordPress 插件。
- Drupal 用户可以安装 Quicklink Drupal 模块。
- Magento 2 用户可以安装 rafaelcg-magento2-quicklink 或 rangerz/magento2-module-quicklink。
- 想要不那么激进的预取?instant.page 在鼠标悬停和触摸开始时预取,就在点击之前。
许可证
根据 Apache-2.0 许可证 授权。