How to Enable Navigation Preload in Service Workers

This guide addresses a hidden cost in Service Worker Caching Strategies, part of Advanced Caching Strategies & CDN Architecture. When a page is controlled by a service worker, every navigation request is routed through the worker's fetch handler. If the worker is not already running, the browser must start it first — load its script, evaluate it, run it — before the handler can even begin the network request. On mobile devices this boot commonly takes 50–200ms, and several hundred milliseconds on slow devices or after the browser has been idle.

For cache-first navigations that cost is offset by skipping the network. For network-first navigations — the right strategy for HTML that must stay fresh — it is pure delay added to TTFB and therefore to LCP. Navigation preload fixes it: the browser starts the navigation request immediately, in parallel with booting the worker, and the handler receives the in-flight response via event.preloadResponse.

Network-first navigation with and without navigation preload Two timelines comparing a navigation where the network request waits for service worker boot with one where navigation preload starts the request in parallel. Network-first navigation with and without navigation preload Without SW boot network request With preload SW boot Preload request request starts at once 0ms 100ms 200ms 300ms 400ms 500ms 600ms 700ms With preload, boot and the network request overlap, so TTFB no longer includes the worker's startup time.

Rapid Diagnosis

  • Check whether navigations are network-first. If the fetch handler calls fetch(event.request) for navigations, they pay boot time.
  • Measure worker startup. In DevTools → Application → Service Workers, or in a trace, look for "ServiceWorker startup" before navigation requests. Navigation Timing's workerStart shows when the worker began; compare it with fetchStart.
  • Compare TTFB with and without the worker. Test with "Bypass for network" enabled: if TTFB improves noticeably, the worker is adding delay.
  • Check for preload support code. Search for navigationPreload.enable().

Root Cause Analysis

1. Worker boot on the critical path. A stopped worker must start before handling the navigation; the browser stops idle workers after short periods.

2. Network-first strategies. They need the network anyway, so waiting for the worker before starting the request is wasted time.

3. Heavy worker scripts. Large worker bundles (importing big libraries, large precache manifests) take longer to evaluate.

4. Slow devices. Boot time scales with device CPU and storage speed.

TTFB added by service worker boot (p75) Bar chart of time added to navigation TTFB by service worker startup on three device classes, and with navigation preload. TTFB added by service worker boot (p75) Desktop 40ms Mid-tier Android 160ms Low-end Android 390ms Any device, with preload 5ms

Step-by-Step Resolution

1. Enable navigation preload on activate

javascript
self.addEventListener('activate', (event) => {
  event.waitUntil((async () => {
    if (self.registration.navigationPreload) await self.registration.navigationPreload.enable();
  })());
});
// trade-off: once enabled, the browser sends a preload request for EVERY
// navigation. If your handler serves some navigations from cache without
// using preloadResponse, those preload requests are wasted traffic.

2. Use the preload response in the fetch handler

javascript
self.addEventListener('fetch', (event) => {
  if (event.request.mode !== 'navigate') return;
  event.respondWith((async () => {
    try {
      const preloaded = await event.preloadResponse;
      if (preloaded) return preloaded;
      return await fetch(event.request);
    } catch {
      return (await caches.match('/offline.html')) ?? Response.error();
    }
  })());
});
// trade-off: if you forget to await event.preloadResponse, the browser logs a
// warning and the preload is cancelled — you get the extra request without the
// benefit. Always consume it, even if you end up using the cache.

Expected outcome: navigation TTFB matches the uncontrolled network path; offline fallback still works.

3. Tell the server it is a preload (optional)

Preload requests carry a Service-Worker-Navigation-Preload header (default value true, configurable with setHeaderValue). The server can use it to return a lighter response (for example, only the content portion for app-shell architectures) — but be careful with caching: vary on the header if responses differ.

4. With Workbox, use its built-in support

Workbox's navigationPreload.enable() plus NetworkFirst or NetworkOnly for navigations uses preloadResponse automatically.

Do you need navigation preload? Decision sequence for whether a service worker benefits from navigation preload. Do you need navigation preload? Navigations are served network-first or network-only? Yes — enable preload yes no Navigations are cache-first or stale-while-revalidate? Usually no, unless you also revalidate from network yes no No fetch handler for navigations at all? No — no boot delay (browser skips the worker) yes no Measure workerStart vs fetchStart to decide

Verification

In DevTools, navigation requests should show "(navigation preload)" or appear as preload requests in the Network panel, starting before the worker's fetch handler runs. Compare TTFB in RUM for controlled navigations before and after: the gap between controlled and uncontrolled navigations should shrink to near zero. Check server logs for the Service-Worker-Navigation-Preload header to confirm preloads are sent.

Worked Example: A News PWA

A news PWA used a network-first strategy for article navigations to keep content fresh, with an offline fallback. RUM showed that navigations controlled by the worker had TTFB 210ms higher at p75 on Android than uncontrolled ones — almost entirely worker boot. Enabling navigation preload and consuming preloadResponse eliminated the gap; LCP p75 for returning visitors improved by about 190ms, and offline behaviour was unchanged.

Monitoring Worker Overhead in the Field

Navigation Timing exposes workerStart, which records when the service worker began handling the navigation (or started booting). Send fetchStart - workerStart and responseStart - fetchStart with your RUM beacon for controlled navigations, segmented by device class. The first measures boot overhead; the second network time. After enabling preload, boot overhead should no longer add to TTFB — if it still does, check that the handler awaits preloadResponse and that preload is enabled for the active registration.

Combining Preload With Cached Fallbacks

Navigation preload pairs naturally with a race between network and cache. For pages where a slightly stale version is acceptable, start from the preload response but fall back to the cached copy if the network has not answered within a short timeout; when the preload response does arrive, store it in the cache for next time. That gives fresh content on good connections, fast cached content on slow ones, and an offline fallback when there is no connection at all — all without waiting for the worker to boot before the network request starts.

Common Mistakes

  • Enabling preload but not using preloadResponse. Doubles requests for no gain.
  • Using preload with cache-first navigations. Wasted network requests for responses you never use.
  • Ignoring the header in caching. If the server returns different content for preload requests, add Vary: Service-Worker-Navigation-Preload.
  • A huge worker script. Preload hides boot time for navigations, but a heavy worker still costs CPU and delays other intercepted requests.

Edge Cases

Static Routing API. Newer Chromium versions support declaring routes the worker should not intercept (registerRouter in install), letting navigations bypass the worker entirely when it adds nothing.

Redirects. Preload responses follow redirects like normal navigations; check that redirect handling in the worker does not discard the preload.

POST navigations. Preload only applies to GET navigations.

Browser support. Navigation preload is supported in Chromium, Firefox and Safari 15.4+; feature-detect with self.registration.navigationPreload.

FAQ

Why not just keep the service worker running?

Browsers stop idle workers to save memory and battery; you cannot keep them alive. Navigation preload is the designed solution for the boot cost.

Does preload help subresource requests?

No — it applies only to navigation requests. Subresources requested after the page loads are handled by an already-running worker, so boot time is not an issue for them.

Is the preload request cached by the HTTP cache?

It behaves like a normal navigation request with respect to HTTP caching, so Cache-Control on HTML still applies. Most HTML policies revalidate, so it usually reaches the CDN.

Should cache-first apps disable preload?

Yes, if no navigation uses the network response. If you use stale-while-revalidate for navigations (serve cache, update from network), preload can supply the network half.

Can the server respond faster to preload requests?

It can return a partial response (content only) when the header is present, which app-shell architectures use to save bytes. Ensure caches distinguish the two response types.

How do I test boot time locally?

Stop the worker in DevTools (Application → Service Workers → stop) before navigating, with CPU throttling enabled. The navigation then pays the full boot cost, as it often does for real users.