即时导航体验

使用 Service Worker 对传统的预提取技术进行补充。

德米安·伦祖利
Demián Renzulli
Gilberto Cocchi
Gilberto Cocchi

在网站上执行某项任务通常涉及多个步骤。例如,在电子商务网站上购买产品可能涉及搜索产品、从结果列表中选择产品、将产品添加到购物车以及通过结账来完成操作。

用技术术语来说,在不同的网页之间移动意味着发出导航请求。一般而言,您不希望使用长期有效的 Cache-Control 标头来缓存导航请求的 HTML 响应。通常应通过网络通过 Cache-Control: no-cache 来满足请求,以确保 HTML 以及后续网络请求链是(合理)最新的。 不幸的是,每次用户导航到新网页时都必须关闭网络,这意味着每次导航可能很慢,至少,这表示浏览速度不够快。

若要加快处理这些请求的速度,您可以预先判断用户的操作,以便提前请求相应网页和资源,并在缓存中将其保留一小段时间,直到用户点击这些链接。这种技术称为预提取,通常通过向网页添加 <link rel="prefetch"> 标记(指明要预提取的资源)来实现。

在本指南中,我们将探索使用 Service Worker 作为传统预提取技术补充的不同方式。

生产案例

MercadoLibre 是拉丁美洲最大的电子商务网站。为了加快导航速度,它们会在流程的某些部分动态注入 <link rel="prefetch"> 标记。例如,在列表页面中,当用户滚动到列表底部时,它们会立即获取下一个结果页面:

MercadoLibre 商品详情页第一页和第二页的屏幕截图,以及将两者连接起来的链接预提取代码。

系统会以“最低”优先级请求预提取的文件,这些文件会在 HTTP 缓存内存缓存中存储一段时间(具体取决于资源是否可缓存),具体时长因浏览器而异。例如,从 Chrome 85 开始,此值为 5 分钟。资源会保留大约 5 分钟,之后,系统将应用资源的常规 Cache-Control 规则。

使用 Service Worker 缓存有助于将预提取资源的生命周期延长到 5 分钟以上。

例如,意大利体育门户网站 Virgilio Sport 使用 Service Worker 在主页中预提取最热门的帖子。它们还使用 Network Information API 来避免为使用 2G 连接的用户进行预提取。

Virgilio Sport 徽标。

因此,经过 3 周多的观察,Virgilio Sport 的报道页面加载时间缩短了 78%,报道展示次数也增加了 45%

Virgilio Sport 首页和文章页面的屏幕截图,其中包含预提取后的影响指标。

使用 Workbox 实现预缓存

在下一部分中,我们将使用 Workbox 展示如何在 Service Worker 中实现不同的缓存技术,这些技术可以用做对 <link rel="prefetch"> 的补充,甚至替代它,通过将此任务完全委托给 Service Worker。

1. 预缓存静态页面和页面子资源

预缓存是 Service Worker 在安装时将文件保存到缓存的功能。

在以下情况下,使用预缓存来实现与预提取类似的目标:提高导航速度。

预缓存静态页面

对于在构建时生成的页面(例如 about.htmlcontact.html)或完全静态的网站,用户只需将网站的文档添加到预缓存列表中,这样用户每次访问它们时,它们就已存在于缓存中:

workbox.precaching.precacheAndRoute([
  {url: '/about.html', revision: 'abcd1234'},
  // ... other entries ...
]);

预缓存页面子资源

预缓存网站的不同版块可能使用的静态资源(例如 JavaScript、CSS 等)是一种通用的最佳实践,可以提高预提取情况的效果。

为了加快电子商务网站的导航速度,您可以在产品信息页面中使用 <link rel="prefetch"> 标记,为产品信息页面中的前几项产品预提取商品详情页面。如果您已预缓存商品页面子资源,这样可以更快地进行导航。

为此,请执行以下操作:

  • 向页面添加 <link rel="prefetch"> 标记:
 <link rel="prefetch" href="/phones/smartphone-5x.html" as="document">
  • 将页面子资源添加到 Service Worker 中的预缓存列表:
workbox.precaching.precacheAndRoute([
  '/styles/product-page.ac29.css',
  // ... other entries ...
]);

2. 延长预提取资源的生命周期

如前所述,<link rel="prefetch"> 会提取 HTTP 缓存中的资源并将其保留一段有限的时间,之后,系统将应用针对资源的 Cache-Control 规则。从 Chrome 85 开始,此值为 5 分钟。

借助 Service Worker,您可以延长预提取页面的生命周期,同时具有使这些资源可供离线使用的额外优势。

在前面的示例中,可以对使用 Workbox 运行时缓存策略预提取商品页面的 <link rel="prefetch"> 进行补充。

为此,请执行以下操作:

  • 向页面添加 <link rel="prefetch"> 标记:
 <link rel="prefetch" href="/phones/smartphone-5x.html" as="document">
  • 在 Service Worker 中针对以下类型的请求实现运行时缓存策略:
new workbox.strategies.StaleWhileRevalidate({
  cacheName: 'document-cache',
  plugins: [
    new workbox.expiration.Plugin({
      maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days
    }),
  ],
});

在本例中,我们选择使用 stale-while-revalidate 策略。在此策略中,可以同时从缓存和网络请求页面。响应来自缓存(如果有),否则来自网络。缓存会始终随每个成功请求的网络响应而保持同步。

3. 将预提取委托给 Service Worker

在大多数情况下,最好的方法是使用 <link rel="prefetch">。该标记是一种资源提示,旨在尽可能地提高预提取的效率。

但在某些情况下,最好将此任务完全委托给 Service Worker。例如:如需在客户端呈现的商品详情页面中预提取前几个商品,可能需要根据 API 响应在页面中动态注入多个 <link rel="prefetch"> 标记。这可能会短暂地占用页面主线程上的时间,并增加实现的难度。

在这种情况下,可以使用“页面与 Service Worker 的通信策略”,将预提取任务完全委托给 Service Worker。此类通信可使用 worker.postMessage() 实现:

与 Service Worker 进行双向通信的页面图标。

Workbox Window 软件包简化了此类通信,从而抽象化了正在执行的底层调用的许多细节。

可以通过以下方式实现使用 Workbox 窗口进行预提取:

  • 在页面中:调用 Service Worker,并向其传递消息类型和要预提取的网址列表:
const wb = new Workbox('/sw.js');
wb.register();

const prefetchResponse = await wb.messageSW({type: 'PREFETCH_URLS', urls: […]});
  • 在 Service Worker 中:实现一个消息处理程序,为每个要预提取的网址发出 fetch() 请求:
addEventListener('message', (event) => {
  if (event.data.type === 'PREFETCH_URLS') {
    // Fetch URLs and store them in the cache
  }
});