# 导航预加载与 Service Worker 启动时间

> 为什么 Service Worker 的启动时间会加进导航的 Time to First Byte、导航预加载如何让请求与启动重叠，以及它在什么情况下没有收益。

导航到一个由带 `fetch` 处理函数的 Service Worker 控制的页面时，网络请求要等 worker 启动并运行处理函数之后才能发出，所以尚未运行的 worker 会把自己的启动时间加进响应。导航预加载让浏览器在启动 worker 的同时发出导航请求，并把响应作为 `event.preloadResponse` 交给处理函数，这样对于来自网络的响应，启动就不再在关键路径上。

## 工作原理

Service Worker 空闲时会被停止、按需启动。web.dev 的测量给出的启动时间约为桌面 50 ms、中端手机接近 250 ms、慢速或被节流的设备超过 500 ms（[Speed up service worker with navigation preloads](https://web.dev/blog/navigation-preload)，web.dev）。没有预加载时这段时间与请求串行；有预加载时浏览器立即发出请求，附加 `Service-Worker-Navigation-Preload: true` 头让服务器能区分两者，并在处理函数索取时以该响应兑现 `event.preloadResponse`。该特性按注册用 `registration.navigationPreload.enable()` 启用，通常在 `activate` 中；头的值可用 `setHeaderValue()` 更改，让服务器对预加载请求返回部分响应（内容片段而不是整页）。支持情况为 Chrome 59、Firefox 99、Safari 15.4（BCD `api.NavigationPreloadManager`），方法级参考见 [Service Worker 条目](/zh/reference/service-worker/navigation-preload/)。

### 支持位置

`NavigationPreloadManager` 存在于 Chrome 59、Firefox 99 与 Safari 15.4，并且都与 `FetchEvent.preloadResponse` 一起到来（BCD `api.NavigationPreloadManager`、`api.FetchEvent.preloadResponse`），仅限安全源。没有它的引擎里 `registration.navigationPreload` 为 `undefined`，`event.preloadResponse` 以 `undefined` 兑现，于是处理函数走兜底路径。

### TTFB 里的时间去了哪里

以 `PerformanceNavigationTiming.responseStart` 测量的 Time to First Byte 包含从 `workerStart` 到 `fetchStart` 的区间，即浏览器启动 worker 的时间。从 Cache Storage 应答的重复访问中这个区间依然存在，但响应不需要网络，所以 TTFB 是启动加一次缓存读取。由网络应答的访问中，预加载让启动与请求往返重叠，TTFB 大约减少一个启动时间。

### 预加载无益的情况

- **缓存优先的导航。** 处理函数从 Cache Storage 应答时，预加载的网络响应没用上，请求浪费了带宽；解决办法是不启用预加载，或只对走网络的作用域启用。
- **已经在运行的 worker。** 被最近事件保持存活的 worker 没有什么可启动，预加载不改变任何东西。
- **`Vary` 与缓存。** 预加载请求与普通导航只差一个头，只按 URL 缓存的 CDN 可能把预加载形态的部分响应提供给普通导航。两种响应不同时服务器要带上 `Vary: Service-Worker-Navigation-Preload`。

处理函数返回了别的响应时，未使用的 `preloadResponse` 会被取消，除非处理函数用 `event.waitUntil()` 保持它；让它被取消是正确的默认行为。

## 示例

前两个示例是 Service Worker 代码；第三个在页面里测量效果。

### 启用预加载并使用预加载的响应

在 `activate` 中启用并加特性检查；在 `fetch` 中优先用预加载的响应，它以 `undefined` 兑现时退回 `fetch()`，非导航请求和不支持该特性的引擎都是这种情况。

```js
self.addEventListener('activate', (event) => {
  event.waitUntil((async () => {
    if ('navigationPreload' in self.registration) {
      await self.registration.navigationPreload.enable();
    }
  })());
});

self.addEventListener('fetch', (event) => {
  if (event.request.mode !== 'navigate') return;
  event.respondWith((async () => {
    const preloaded = await event.preloadResponse;
    if (preloaded) return preloaded;
    return fetch(event.request); // 这里没有预加载：普通网络请求
  })());
});
```

对非导航请求提前返回，处理函数就不会拦截它无需触碰的子资源，也缩短了它们的路径。

### 向预加载请求提供内容片段

用 `setHeaderValue('fragment')` 后，服务器可以只返回外壳需要的那部分，处理函数把它拼进缓存的外壳，比整页快、比两次请求省。

```js
// Service Worker，activate 中
await self.registration.navigationPreload.setHeaderValue('fragment');

// Service Worker，导航的 fetch 中
const shell = await caches.match('/shell.html');
const preloaded = await event.preloadResponse;
if (shell && preloaded) {
  const fragment = await preloaded.text();
  return new Response(renderShell(await shell.text(), fragment), { headers: { 'content-type': 'text/html' } });
}
```

服务器读取 `Service-Worker-Navigation-Preload: fragment`，并在响应上设置 `Vary: Service-Worker-Navigation-Preload`，让缓存把片段和整页分开。

### 在页面里测量启动时间

由 Service Worker 处理的导航上 `workerStart` 非零；它与 `fetchStart` 的差就是预加载所重叠的区间。

```js
const [nav] = performance.getEntriesByType('navigation');
if (nav && nav.workerStart > 0) {
  console.log('worker 启动', (nav.fetchStart - nav.workerStart).toFixed(1), 'ms');
} else {
  console.log('这次导航没有 Service Worker'); // 预加载没有可隐藏的东西
}
```

把这个数字与 `responseStart - requestStart` 比较，就能看出在某台设备上是启动还是服务器主导了 TTFB，这个数字决定了预加载是否值得在缓存命中时浪费的那次请求。

:::observed
预加载的导航在 Chrome DevTools 的 Network 面板里显示为带请求头 `Service-Worker-Navigation-Preload: true`（或传给 `setHeaderValue()` 的值）的文档请求，可在 **Headers > Request Headers** 下看到；worker 自己的 `fetch(event.request)` 兜底则会显示为第二个带齿轮图标的文档请求，只看到第一个即证实 `preloadResponse` 被使用了。在注册拥有活跃 worker 之前调用 `navigationPreload.enable()` 会以 `InvalidStateError` 拒绝，这就是该调用放在 `activate` 而不是 `install` 的原因（[NavigationPreloadManager](https://developer.mozilla.org/en-US/docs/Web/API/NavigationPreloadManager)，developer.mozilla.org）。
:::

## 另请参阅

- [Speed up service worker with navigation preloads](https://web.dev/blog/navigation-preload)（web.dev）
- [Service Workers: NavigationPreloadManager](https://w3c.github.io/ServiceWorker/#navigation-preload-manager)（w3.org）
- [导航预加载（Service Worker API 参考）](/zh/reference/service-worker/navigation-preload/)
- [应用外壳（app shell）模型](/zh/reference/performance/app-shell/)
- [PerformanceObserver 与绘制计时](/zh/reference/performance/startup-performance/)