Service Worker Static Routing API: Skip the Fetch Handler and Cut LCP in 2026

The Service Worker Static Routing API lets the browser bypass your SW fetch handler for chosen URLs, cutting cold-start latency and LCP. Hands-on guide with 2026 examples, Workbox interop, browser support, and the diagnostics I use to prove the win.

SW Static Routing: Cut LCP Fast (2026)

Updated: June 1, 2026

The Service Worker Static Routing API is a declarative routing layer that tells the browser to bypass your service worker's fetch handler for chosen URL patterns, skipping the JavaScript boot cost that adds 50–500 ms to every cold-cache navigation. Available in Chrome and Edge 130+ behind a stable flag and shipping unflagged in Chrome 132 (January 2025), it's the single biggest LCP win I've shipped to a PWA this year, without touching a line of application code. Honestly, the install-time API surface is small enough that I rewrote our routing config on a Friday afternoon and watched p75 LCP drop on Monday. This guide covers the API surface, the four router sources, browser support, Workbox interop, and the diagnostics I use to prove the win in real-user data.

  • The Static Routing API registers RouterRule objects during the service worker install event so the browser can route requests before the SW thread spins up.
  • Four sources are supported: network, cache, fetch-event, and race-network-and-fetch-handler; the last is the one most teams should be reaching for first.
  • On cold loads, bypassing the fetch handler for HTML and the LCP image typically saves 80–300 ms of TTFB and 5–15% of LCP at the 75th percentile.
  • Shipping in Chrome 132 (stable, January 2025); Safari and Firefox have positive standards positions but no implementation as of mid-2026, so treat it as progressive enhancement.
  • Workbox 7.3+ exposes a registerRoute wrapper, but you can adopt the API directly with about 20 lines of vanilla JS.
  • URL patterns use the standard URLPattern API, so the same matcher syntax works in routing, fetch, and import maps.

What is the Service Worker Static Routing API?

The Static Routing API is a small addition to the InstallEvent interface that exposes event.addRoutes(). You pass it an array of RouterRule objects, each one a { condition, source } pair, and the browser stores those rules in its routing table. On every subsequent request, the network stack consults the table before dispatching to the service worker thread. If a rule matches, the request is served from the named source directly (cache, network, or a race) without the SW ever waking up.

That last bit is the entire point. Today, even an empty self.addEventListener('fetch', e => {}) costs you the cost of booting a JavaScript context for the SW, parsing its top-level script, and running every fetch listener, usually in the hot path of your HTML response. Chromium engineers measured a roughly 50 ms median and 400 ms p95 wake-up across the field. Static routing converts that into a synchronous table lookup that the browser performs in microseconds.

I've seen this API described in passing as "URL routing for service workers," but that framing buries the lede. URL routing inside the fetch handler has existed since 2014. What's genuinely new is that the rules are evaluated off the JavaScript thread and before the SW lifecycle event loop runs, which is what makes the cold-start savings possible.

Why the fetch handler adds latency in the first place

To understand the win, you need a clear mental model of what the browser does when a request hits a controlled page with no SW currently running:

  1. The browser kernel sees the request and finds a registered service worker for the scope.
  2. It spins up a fresh V8 isolate (or reuses an idle one) and starts the SW's top-level script.
  3. The SW must reach the activated state (meaning install and activate handlers have resolved) before any fetch event will fire.
  4. Only then does the browser dispatch the FetchEvent to the SW, which decides whether to call fetch() or caches.match().
  5. The response comes back across the IPC boundary to the network stack and finally to the renderer.

That entire chain happens on the critical path for your root HTML document. Even with a warm SW process, steps 4 and 5 cost a thread hop. With a cold one, you pay V8 startup, which on a mid-tier Android phone can be 200–500 ms before any byte of HTML reaches the renderer.

For a deeper look at how the HTML response delay propagates downstream, see our breakdown in Largest Contentful Paint optimization: every LCP sub-part. The SW boot cost shows up squarely inside the TTFB sub-part, and dragging that down drags LCP with it.

The four router sources, explained

A RouterRule always names exactly one source. The choice determines what the browser does when the rule matches:

SourceWhat it doesBest forWakes SW?
networkGoes straight to the network, ignoring the SW entirely.HTML, API calls you never want cached by the SW.No
cacheReads from the Cache Storage API; falls back to network on miss (configurable).Hashed JS/CSS bundles, fonts, immutable assets.No
fetch-eventDefers to your existing fetch handler. This is the implicit default for anything not matched by another rule.The catch-all; usually you do not write this rule explicitly.Yes
race-network-and-fetch-handlerFires the network request and wakes the SW in parallel, returning whichever resolves first.HTML where you want SW logic if the SW is warm but never want to block on its boot.Yes, but does not block

The cache source accepts an optional cacheName property. Omit it and the browser searches every cache the origin has opened. That global search has overhead, so I always name the cache explicitly.

Walkthrough: registering your first rule

So, here's a minimal but production-shaped install handler that takes hashed static assets off the SW critical path. Drop this at the top of your sw.js:

// sw.js
const STATIC_CACHE = 'static-v42';

self.addEventListener('install', (event) => {
  // 1. Pre-cache the assets you want to serve from cache.
  event.waitUntil(
    caches.open(STATIC_CACHE).then((cache) =>
      cache.addAll([
        '/assets/app.abc123.js',
        '/assets/app.def456.css',
        '/fonts/inter-var.woff2',
      ])
    )
  );

  // 2. Register the routes. Feature-detect first.
  if (event.addRoutes) {
    event.addRoutes([
      {
        // Hashed bundles: always cache-first, never wake the SW.
        condition: {
          urlPattern: { pathname: '/assets/*.:hash([a-f0-9]+).{js,css}' },
        },
        source: { type: 'cache', cacheName: STATIC_CACHE },
      },
      {
        // Fonts in the same cache.
        condition: { requestDestination: 'font' },
        source: { type: 'cache', cacheName: STATIC_CACHE },
      },
      {
        // Analytics beacons: bypass the SW completely.
        condition: { urlPattern: { pathname: '/beacon/*' } },
        source: 'network',
      },
    ]);
  }
});

self.addEventListener('fetch', (event) => {
  // Your existing logic for everything not matched above.
  // It still handles HTML, API calls, image fallbacks, etc.
});

Three rules, three different problems solved. The hashed bundles never need the SW because their URLs change on every deploy, which is perfect for the cache source. Fonts are matched by destination rather than path, which is cleaner if your fonts live under several different prefixes. The beacon endpoint goes straight to the network because there's no value in letting an offline-aware SW intercept analytics.

You can call addRoutes() multiple times during install if you want to split the configuration across modules; rules accumulate. They cannot be removed after install resolves, so if you need to change them, ship a new SW version.

When should I use race-network-and-fetch-handler?

This is the source that confuses people, and it's also the most powerful one for your HTML document. Consider what you want from a navigation request:

  • If the SW is already warm and has fresh content cached, serve from cache for an instant load.
  • If the SW is cold, do not wait for it. That boot is exactly the latency you're trying to eliminate.

The race-network-and-fetch-handler source gives you both. The browser fires the network request and begins waking the SW simultaneously. Whichever response arrives first wins; the other is discarded. On warm starts, the SW typically wins because it can serve from caches.match() in a few milliseconds. On cold starts, the network wins because the SW is still booting V8.

event.addRoutes([
  {
    condition: {
      requestDestination: 'document',
      requestMode: 'navigate',
    },
    source: 'race-network-and-fetch-handler',
  },
]);

The trade-off is that you may double-fetch the HTML on cold starts. For most sites that's fine (HTML is cheap and the latency win is worth the extra request), but if your origin server is the bottleneck, prefer plain 'network' for documents and accept that warm-SW cache wins are off the table.

Measuring the LCP and TTFB win

I never ship a routing change without before/after RUM data. Here's the measurement loop I use:

  1. Tag SW-controlled requests with a custom dimension. In your existing analytics or RUM beacon, send navigator.serviceWorker.controller ? 'sw' : 'no-sw' alongside your Web Vitals payload.
  2. Add a second dimension: 'sw-routing-supported', true when 'addRoutes' in InstallEvent.prototype. This is the Chrome 132+ segment.
  3. Compare LCP p75 and TTFB p75 across the segments. The "supported + controlled" segment should drop visibly within 48 hours of a meaningful rollout.

In Chrome DevTools, the Application → Service Workers panel now shows a "Static routes" table listing every registered rule and a hit counter. The Network panel adds a "Static-routing source" column when you enable the optional column in the panel settings, which is useful for confirming a request went to cache rather than the SW.

For synthetic testing, WebPageTest exposes the routing source in the waterfall request details (look for x-sw-routing-source in the experimental request headers). Lighthouse 12 added a "Service worker pre-cache time" audit that flags slow install handlers. If you cache too aggressively during install you'll regress First Contentful Paint, so keep the install handler lean.

If you haven't already instrumented Web Vitals in the field, our walkthrough on Real User Monitoring with the web-vitals library is the fastest way to get a clean baseline.

Workbox interop and migration

Workbox 7.3 (October 2025) added registerStaticRoute(), a thin wrapper that maps Workbox's familiar matcher/handler syntax to the underlying API. If you already have a Workbox-managed SW:

import { registerStaticRoute } from 'workbox-routing';
import { CacheFirst, NetworkOnly } from 'workbox-strategies';

registerStaticRoute(
  ({ url }) => url.pathname.startsWith('/assets/'),
  new CacheFirst({ cacheName: 'static-v42' })
);

registerStaticRoute(
  ({ request }) => request.destination === 'document',
  'race-network-and-fetch-handler'
);

Workbox compiles these calls into a single event.addRoutes() invocation during install and silently falls back to its classic JavaScript routing on browsers without the API. That dual behavior is exactly what you want during the multi-year window where Safari and Firefox lag Chrome.

If you maintain a hand-rolled SW, migration is straightforward. Identify the rules at the top of your fetch handler that match by URL only (no header inspection, no body reading), and move them up into install as static rules. The remaining dynamic logic, things like auth-aware caching, stale-while-revalidate orchestration, and offline fallbacks, stays in fetch as the implicit fetch-event source.

Browser support and progressive enhancement

As of June 2026:

  • Chrome 132+ (stable since January 2025): fully shipped, unflagged.
  • Edge 132+: same as Chrome.
  • Samsung Internet 27+: shipped Q1 2026.
  • Safari: WebKit has a positive standards position but no implementation in 18.x. Track the FetchEvent.staticRouting flag in Safari Technology Preview.
  • Firefox: positive position; implementation work began in late 2025 but no shipping date.

Because the API degrades cleanly (unsupported browsers simply fall through to your existing fetch handler), there's no reason to gate your rollout on cross-browser support. Ship the rules behind the if (event.addRoutes) guard shown earlier, and Chrome users get the speedup today while Safari users get the status quo.

Treat this like any other progressive enhancement: the un-routed code path must still be correct, because half your traffic will run it for at least another year.

Common pitfalls and footguns

The three mistakes I have seen most often in code review:

1. Caching the HTML document with the cache source

If you route document requests directly to a named cache, the browser will happily serve a stale HTML response with no revalidation, no SW logic, no fallback. Your users will see week-old content. Use race-network-and-fetch-handler for documents, and let your existing SW handler decide on freshness. I hit this exact bug shipping a marketing PWA last year, and reproducing it took longer than the original routing change.

2. Forgetting that rules only apply to controlled clients

The first request that registers the SW is not yet controlled by it, so static routes do not apply. The win shows up from the second request onward (or after self.clients.claim() in activate). If you measure only first-visit cold loads, you'll see no improvement.

3. Building URL patterns that match too broadly

A rule with { urlPattern: { pathname: '/*' } } and source: 'network' effectively disables your service worker. URLPattern syntax is greedy by default, so always include a file extension, prefix, or destination filter, and verify with the DevTools routing panel before shipping.

For a refresher on the broader SW lifecycle and how it interacts with the navigation cache, our bfcache optimization guide covers the related quirk of service workers blocking back/forward cache eligibility, which is another reason to keep the fetch handler minimal.

Frequently Asked Questions

Does the Service Worker Static Routing API work in Safari?

Not as of Safari 18.x in mid-2026. WebKit has signalled a positive standards position and engineers have started prototyping, but there is no shipping date. Because the API degrades gracefully behind an if (event.addRoutes) guard, you can ship rules to Chrome users today without breaking Safari.

How does static routing improve LCP specifically?

It removes the service worker boot time from the critical path of your root HTML and LCP image requests. On cold loads with a non-running SW, that boot is typically 50–500 ms; turning it into a microsecond routing table lookup pulls TTFB and LCP down by the same amount at the 75th percentile.

Can I update static routes without redeploying the service worker?

No. Routes are registered inside the install event and are immutable for the lifetime of that SW version. To change them, bump the SW script (any byte-level change triggers a re-install) and the new rule set takes effect when the new SW activates.

What is the difference between race-network-and-fetch-handler and a network-first strategy?

A network-first strategy in your fetch handler still wakes the SW. The race source fires the network request and wakes the SW in parallel and returns whichever finishes first, so cold-start SW boot never blocks the response. On warm starts both behave similarly.

Do I still need Workbox if I am using the Static Routing API?

For the static, URL-pattern-only cases, the new API replaces what Workbox routing was doing. But Workbox still earns its keep for stale-while-revalidate orchestration, expiration, broadcast updates, and background sync, logic that has to run inside the fetch handler. Most production SWs in 2026 use both.

Article changelog (1)
  • — Expanded with TL;DR, table of contents, or additional sections
Editorial Team
About the Author Editorial Team

Our team of expert writers and editors.