Service Worker Routing β€” Part 1: Fetch Handler & Strategies 🚦

8 min read

Lecture 1: Routing Cache Fallback Offline πŸ”€

A Word of Warning Before the Fetch Handler

Once you add a fetch handler, the service worker stops being passive β€” it now intercepts every request. This makes development significantly harder:

"As soon as there's a caching service worker in place, you'll change your JavaScript file, hit refresh, even shift-refresh, and it won't load. You'll go nuts until you remember β€” I have a caching service worker."

Kyle's practical advice on timing: don't install an aggressive caching service worker on day one of a project. Wait until you're close to deployment. The overhead of working around it during active development isn't worth it.

His personal workaround during development: a kill switch flag at the top of sw.js:

JavaScript
const DISABLE_SW = true; // flip to false before deploying async function main() { if (DISABLE_SW) return; // bail out entirely // ... rest of SW logic }

Develop freely, flip the flag when ready to ship. Simple, effective.


The Fetch Event Handler

JavaScript
self.addEventListener('fetch', onFetch); function onFetch(event) { event.respondWith(router(event.request)); } async function router(request) { let url = new URL(request.url); let reqURL = url.pathname; let cache = await caches.open(cacheName); // routing logic goes here... }

Key points:

  1. event.respondWith() β€” unlike waitUntil, this tells the SW to handle the response entirely. Whatever promise you pass it becomes the response the browser gets.
  2. new URL(request.url) β€” use the platform's built-in URL parser, not manual string parsing. .pathname gives you just the /path part, matching how Node.js exposes request URLs.
  3. caches.open(cacheName) β€” same as before, open the named cache store so we can read from it.
  4. Delegate to router() β€” same pattern as handleActivation β€” keep the event handler lean, do async work in a named function.

What About External Resources?

A decision point Kyle flags before writing any routing logic: what do you do with requests to other domains (CDNs, third-party APIs)?

Opaque requests (no CORS headers) can pass through but can't be cached meaningfully. Options:

  1. Skip them β€” let external requests pass through untouched, no caching
  2. Cache them β€” requires the external server to send CORS headers
  3. Self-host β€” if a CDN resource is critical for offline, consider hosting it yourself

"If you're loading a framework from a CDN and that's critical for your page to load β€” and you do an opaque request β€” now you have a critical resource of your site that you're not offlining."

For this project: all resources are self-hosted, so we skip this complexity entirely. But in a real project it's a decision that needs to be made deliberately.


Lecture 2: Caching Strategies 🧠

Filter to Same-Origin Requests First

Before any routing logic, only handle requests to your own server:

JavaScript
if (url.origin === location.origin) { // handle this request } // TODO: figure out CORS requests strategy

url.origin vs location.origin β€” if they match, it's your server. Everything else falls through unhandled.


The Four Main Caching Strategies

StrategyHow it worksBest for
Network OnlyAlways fetch, never cacheLive data, always-fresh content
Network First, Cache FallbackTry network β†’ cache on success β†’ cache on failureRegularly changing content that needs offline support
Cache First, Network FallbackServe from cache β†’ fetch if missing β†’ cache resultStatic assets, rarely changing files
Cache + Background UpdateServe cache immediately, fetch in parallel, notify if differentSpeed + freshness balance

The Offline Fallback Problem

What happens when a user is offline, requests a page, and it's not in the cache? Three options:

  1. Return a browser 404 β€” cold, unhelpful
  2. Return a custom 404 β€” better, but wrong message if they're offline
  3. Return a custom offline page β€” the right answer

This is why '/offline' was included in urlsToCache. Two distinct cases:

  • Online + page not found β†’ friendly 404 page
  • Offline + page not in cache β†’ offline page (we can't know if it exists or not)

"I'm really trying to teach you to think like a fisherman, rather than just give you these fish."


Lecture 3: Implementing a Caching Strategy πŸ”§

Network First, Cache as Fallback

JavaScript
async function router(request) { let url = new URL(request.url); let reqURL = url.pathname; let cache = await caches.open(cacheName); if (url.origin === location.origin) { let res; try { let fetchOptions = { method: request.method, headers: request.headers, credentials: 'omit', cache: 'no-store' }; res = await fetch(request.url, fetchOptions); if (res.ok) { await cache.put(reqURL, res.clone()); // clone before caching! return res; } } catch(err) {} // Fallback to cache res = await cache.match(reqURL); if (res) return res.clone(); } }

Key decisions:

  1. request.method and request.headers β€” preserve the original request, don't hardcode GET
  2. cache: 'no-store' β€” don't let the browser store it, we're handling storage ourselves
  3. request.url for fetch, reqURL for cache β€” full URL to the server (preserves query params), pathname as cache key (strips volatile query params)
  4. res.clone() everywhere β€” going to cache AND returning from cache. A response can only be consumed once.
  5. respondWith not waitUntil β€” respondWith intercepts the request and uses the resolved value as the actual HTTP response

Lecture 4: Offline Routing Demo 🎬

Two Tests, Both Passing βœ…

Test 1 β€” Device offline (Network tab β†’ Offline): Navigate to any cached page β†’ loads from cache. CSS, images, all static assets served offline.

Test 2 β€” Server killed (Ctrl+C): User is online but server is unreachable β†’ network requests fail β†’ SW catches failure β†’ cache serves the page β†’ user sees nothing wrong.

Common Bug: Missing new on URL Constructor

JavaScript
let url = URL(request.url); // ❌ silent failure let url = new URL(request.url); // βœ… correct

DevTools Reset Ritual

When something isn't working, this is the full clean slate sequence:

  1. Go back online
  2. Stop the service worker
  3. Unregister it
  4. Clear cache storage
  5. Clear network log
  6. Refresh the page

Lecture 5: Offline Routing Q&A πŸ™‹

Why request.url for Fetch but reqURL for Cache?

JavaScript
res = await fetch(request.url, fetchOptions); // full URL β€” preserves query params await cache.put(reqURL, res.clone()); // pathname only β€” stable cache key

Query params are volatile (tracking IDs, timestamps). Including them in the cache key creates dozens of entries for the same logical page. Send them to the server, strip them from the cache key.

Shift-reload does bypass the service worker β€” but watch out for the double-click race condition. Single shift-reload works as expected.