# Navigation preload：让导航请求不必等待 service worker 启动

> NavigationPreloadManager 如何让浏览器在 service worker 启动的同时并行开始获取导航请求、如何启用它并读取 FetchEvent.preloadResponse、Service-Worker-Navigation-Preload 请求头，以及它自 2022 年 4 月起 Baseline 广泛可用的状态。

**一句话：** Navigation preload 让浏览器在 service worker 启动的同时并行开始获取导航请求，这样即使
worker 需要冷启动，也不会比已在运行的 worker 更慢。

## 它解决的问题

当用户导航到一个受 service worker 控制的页面时，浏览器必须启动 service worker（如果尚未运行）、
向它发送一个 `fetch` 事件，然后等待结果。service worker 在启动完成之前无法处理事件，而当 worker
确实需要从头启动时，这段启动延迟可能会拖慢导航请求的响应——即便最终的响应本可以来自通常非常快
的缓存。

## 启用方式

Navigation preload 通过 `NavigationPreloadManager.enable()` 启用，通常放在 service worker 的
`activate` 事件处理器中，并通过 `ServiceWorkerRegistration.navigationPreload` 做特性检测：

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

如果支持该特性，`self.registration.navigationPreload` 会返回 `NavigationPreloadManager` 对象，
否则返回 `undefined`。

### 实例方法

| 方法 | 作用 |
|---|---|
| `enable()` | 启用 navigation preload；返回一个 resolve 为 `undefined` 的 `Promise`。 |
| `disable()` | 禁用 navigation preload；返回一个 resolve 为 `undefined` 的 `Promise`。 |
| `setHeaderValue()` | 设置随预加载请求发送的 `Service-Worker-Navigation-Preload` HTTP 请求头的值；返回一个空的 `Promise`。 |
| `getState()` | 返回一个 `Promise`，resolve 为一个对象，指示预加载是否启用以及使用的请求头值。 |

## 使用预加载的响应

Navigation preload 必须与 `fetch` 事件处理器配合使用：预加载的结果通过 `FetchEvent.preloadResponse`
读取，这是一个会 resolve 为 `Response`（若该请求未触发预加载则为 `undefined`）的 `Promise`：

```js
addEventListener("fetch", (event) => {
  event.respondWith(
    (async () => {
      const cachedResponse = await caches.match(event.request);
      if (cachedResponse) return cachedResponse;

      const response = await event.preloadResponse;
      if (response) return response;

      return fetch(event.request);
    })(),
  );
});
```

只有当该 service worker 已启用 navigation preload、请求是 `GET` 请求、且请求是导航请求时，
`preloadResponse` 才会 resolve 为一个 `Response`；否则它会 resolve 为 `undefined`。该属性仅在
service worker 内部可用。

## `Service-Worker-Navigation-Preload` 请求头

浏览器会自动在预加载请求中带上 `Service-Worker-Navigation-Preload` 请求头（默认值为 `true`），
让服务器可以区分预加载请求与普通请求。`setHeaderValue()` 可以修改这个值，例如设置为一个缓存版本
ID：

```js
navigator.serviceWorker.ready
  .then((registration) =>
    registration.navigationPreload.setHeaderValue(newValue),
  )
  .then(() => {
    console.log("Done!");
  });
```

如果普通响应与预加载响应可能不同，服务器必须设置 `Vary: Service-Worker-Navigation-Preload`，
才能让缓存行为保持正确。

## 浏览器支持

`NavigationPreloadManager` 与 `FetchEvent.preloadResponse` 需要安全上下文（HTTPS），并且自
2022 年 4 月起属于 Baseline 广泛可用——已在许多设备和浏览器版本中得到充分确立的支持。

## 实用清单

- [ ] 在 `activate` 处理器中启用 navigation preload，并用特性检测加以保护。
- [ ] 在 `fetch` 处理器中 await `event.preloadResponse`；不要在其他地方使用该响应。
- [ ] 如果最终没有使用预加载请求的响应（例如命中了缓存），用 `event.waitUntil()` 让该请求保持存活。
- [ ] 如果普通响应与预加载响应可能不同，在服务器上设置 `Vary: Service-Worker-Navigation-Preload`。

## 相关参考

- [Service Worker 生命周期](/zh/reference/service-worker/lifecycle/) — 启用 navigation preload 的 `activate` 事件
- [Fetch 事件处理](/zh/reference/service-worker/fetch-event/) — 读取 `preloadResponse` 的 `fetch` 事件