Concept in Performance
Navigation preload and service worker boot time
Published
A navigation to a page controlled by a service worker with a fetch handler cannot start its
network request until the worker has started and run the handler, so a worker that is not
already running adds its boot time to the response. Navigation preload tells the browser to send
the navigation request at the same time as it boots the worker, and hands the response to the
handler as event.preloadResponse, which removes the boot from the critical path for responses
that come from the network.
How it works
Section titled “How it works”Service workers are stopped when idle and started on demand. The web.dev measurements put the
boot at around 50 ms on desktop, closer to 250 ms on a mid-range phone, and over 500 ms on a
slow or throttled device (Speed up service worker with navigation preloads,
web.dev). Without preload that time is serial with the request; with preload the browser issues
the request immediately, adds a Service-Worker-Navigation-Preload: true header so the server
can tell the two apart, and resolves event.preloadResponse with the response once the handler
asks for it. The feature is enabled per registration with
registration.navigationPreload.enable(), usually in activate, and the header value can be
changed with setHeaderValue() so a server can return a partial response (a content fragment
instead of a full page) for preload requests. Support is Chrome 59, Firefox 99, and Safari 15.4
(BCD api.NavigationPreloadManager), and the method-level reference is the
service worker entry.
Support position
Section titled “Support position”NavigationPreloadManager is in Chrome 59, Firefox 99, and Safari 15.4, in every case alongside
FetchEvent.preloadResponse (BCD api.NavigationPreloadManager, api.FetchEvent.preloadResponse),
and only on secure origins. In an engine without it registration.navigationPreload is
undefined and event.preloadResponse resolves undefined, so the handler’s fallback path runs.
Where the time goes in TTFB
Section titled “Where the time goes in TTFB”Time to First Byte, as measured by PerformanceNavigationTiming.responseStart, includes
workerStart to fetchStart, the interval in which the browser starts the worker. On a repeat
visit served from Cache Storage the interval still exists but the response needs no network, so
TTFB is the boot plus a cache read. On a visit served from the network, preload overlaps the
boot with the request’s round trip, so TTFB drops by roughly the boot time.
When preload does not help
Section titled “When preload does not help”- Cache-first navigations. If the handler answers from Cache Storage, the preloaded network response is unused and the request wasted bandwidth; the fix is to not enable preload, or to enable it only for scopes that go to the network.
- Workers that are already running. A worker kept alive by a recent event has nothing to boot, and preload changes nothing.
Varyand caching. The preload request differs from a normal navigation only by its header, so a CDN that caches by URL alone can serve a preload-shaped partial response to a normal navigation. The server answers withVary: Service-Worker-Navigation-Preloadwhen the two responses differ.
An unused preloadResponse is cancelled when the handler returns another response unless the
handler keeps it alive with event.waitUntil(); letting it cancel is the right default.
Examples
Section titled “Examples”The first two examples are service worker code; the third measures the effect from the page.
Enabling preload and using the preloaded response
Section titled “Enabling preload and using the preloaded response”Enable in activate, guarded by a feature check; in fetch, prefer the preloaded response
and fall back to fetch() when it resolves undefined, which it does for non-navigation
requests and in engines without the feature.
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); // no preload here: ordinary network request })());});Returning early for non-navigation requests keeps the handler from intercepting subresources it does not need to touch, which also shortens their path.
Serving a content fragment to preload requests
Section titled “Serving a content fragment to preload requests”With setHeaderValue('fragment') the server can return only the part the shell needs and the
handler stitches it into the cached shell, which is faster than a full page and cheaper than
two requests.
// service worker, in activateawait self.registration.navigationPreload.setHeaderValue('fragment');
// service worker, in fetch for navigationsconst 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' } });}The server reads Service-Worker-Navigation-Preload: fragment and sets
Vary: Service-Worker-Navigation-Preload on the response so caches keep the fragment and the full
page apart.
Measuring the boot from the page
Section titled “Measuring the boot from the page”workerStart is non-zero on navigations handled by a service worker; the difference to
fetchStart is the interval preload overlaps.
const [nav] = performance.getEntriesByType('navigation');if (nav && nav.workerStart > 0) { console.log('worker boot', (nav.fetchStart - nav.workerStart).toFixed(1), 'ms');} else { console.log('no service worker on this navigation'); // nothing for preload to hide}Comparing this figure with responseStart - requestStart shows whether the boot or the server
dominates TTFB on a given device, which is the number that decides whether preload is worth
the wasted request on cache hits.
See also
Section titled “See also”- Speed up service worker with navigation preloads (web.dev)
- Service Workers: NavigationPreloadManager (w3.org)
- Navigation preload (service worker API reference)
- The app shell model
- PerformanceObserver and paint timing
Specifications
| Specification | Status |
|---|---|
| None. | |