跳转到内容

作为性能修复手段的导航预加载:在 service worker 启动的同时发出网络请求

一句话: 若不启用导航预加载,service worker 的启动时间会横在导航网络请求前面;根据 web.dev,启用预加载后,浏览器会在 service worker 仍在启动时就发出网络请求,因此启动时 间不再阻塞网络请求。

对于一个由带有 fetch 事件处理器的 service worker 控制的页面,浏览器必须向该 worker 派发这个事件并等待响应——如果 worker 尚未运行,就意味着要先启动它。根据 web.dev 的数据,这段启动时间取决于设备和当前 状况:桌面端通常约 50ms,移动端更接近 250ms,在缓慢或 CPU 受限的设备上甚至可能超过 500ms。如果不启用导航预加载,导航的网络请求要等到 worker 启动完毕并开始执行 fetch 处理器之后才会发出——启动时间因此被直接叠加到获取响应所需的时间上。

Time to First Byte 包含浏览器在响应到达前,检查是否有活跃的 service worker 能提供匹配 响应所花的时间。当这项检查需要一次冷启动时,启动延迟就成了 TTFB 的一部分。根据 web.dev, 导航预加载会在 service worker 启动的同时就发出网络请求;启动延迟依然存在,但它不再阻塞 网络请求。

addEventListener('activate', (event) => {
event.waitUntil(
(async () => {
if ('navigationPreload' in self.registration) {
await self.registration.navigationPreload.enable();
} else {
// 不支持:无需启用。下面的 fetch 处理器依旧能工作,只是要等 worker
// 启动完毕后导航请求才会发出——和没有这段代码时的行为一样。
return;
}
})(),
);
});
addEventListener('fetch', (event) => {
event.respondWith(
(async () => {
const preload = await event.preloadResponse;
if (preload) return preload;
return fetch(event.request);
})(),
);
});

'navigationPreload' in self.registration 即为特性检测写法; 当该请求未触发预加载时(例如非 GET 或非导航请求),event.preloadResponse 会解析为 undefined,因此处理器必须始终回退到普通的 fetch()。MDN 自己的 FetchEvent.preloadResponse 示例用 event.waitUntil(preloadResponsePromise.catch(() => undefined)) 让已启动但未使用的预 加载请求保持存活,以免在处理器最终改为从缓存提供响应时它被取消。

根据 MDN,NavigationPreloadManager 需要安全上下文(HTTPS),自 2022 年 4 月起属于 Baseline 广泛可用。

导航预加载并非普遍有益。根据 web.dev,如果页面直接从 Cache Storage API 提供,service worker 的启动时间就不算显著问题,因为缓存中的响应本就无需等待网络即可获得——对这类响应 而言,预加载本要隐藏的那段延迟从一开始就不是瓶颈。

  • 在采用预加载之前,先测量你的导航响应究竟来自缓存还是网络——完全预缓存的 app shell 从中获益较少。
  • activate 中启用它,并以 if ('navigationPreload' in self.registration) 作为保护。
  • 始终 await event.preloadResponse,并在其解析为 undefined 时回退到 fetch()
  • 按照 MDN 的 FetchEvent.preloadResponse 示例,用 event.waitUntil() 让已启动但 未使用的预加载请求保持存活,避免在你最终改为从缓存提供响应时它被取消。
  • 如果预加载响应与普通响应可能不同,请在服务器上设置 Vary: Service-Worker-Navigation-Preload,以免缓存把两者混淆。