即时导航体验

利用 Service Worker 补充传统的预提取技术。

Demián Renzulli
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 的产品信息页面 1 和 2 的屏幕截图,以及连接这两个页面的链接预提取代码。

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

使用 Service Worker 缓存可以将预提取资源的生命周期延长到超过五分钟窗口。

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

Virgilio Sport 徽标。

因此,Virgilio Sport 在 3 周多时间的观察中发现,浏览文章的加载时间缩短了 78%,文章展示次数增加了 45%

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

使用 Workbox 实现预缓存

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

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
  }
});