Service Worker Routing β Part 1: Fetch Handler & Strategies π¦
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:
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
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:
event.respondWith()β unlikewaitUntil, this tells the SW to handle the response entirely. Whatever promise you pass it becomes the response the browser gets.new URL(request.url)β use the platform's built-in URL parser, not manual string parsing..pathnamegives you just the/pathpart, matching how Node.js exposes request URLs.caches.open(cacheName)β same as before, open the named cache store so we can read from it.- Delegate to
router()β same pattern ashandleActivationβ 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:
- Skip them β let external requests pass through untouched, no caching
- Cache them β requires the external server to send CORS headers
- 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:
if (url.origin === location.origin) {
// handle this request
}
// TODO: figure out CORS requests strategyurl.origin vs location.origin β if they match, it's your server. Everything else falls through unhandled.
The Four Main Caching Strategies
| Strategy | How it works | Best for |
|---|---|---|
| Network Only | Always fetch, never cache | Live data, always-fresh content |
| Network First, Cache Fallback | Try network β cache on success β cache on failure | Regularly changing content that needs offline support |
| Cache First, Network Fallback | Serve from cache β fetch if missing β cache result | Static assets, rarely changing files |
| Cache + Background Update | Serve cache immediately, fetch in parallel, notify if different | Speed + 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:
- Return a browser 404 β cold, unhelpful
- Return a custom 404 β better, but wrong message if they're offline
- 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
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:
request.methodandrequest.headersβ preserve the original request, don't hardcode GETcache: 'no-store'β don't let the browser store it, we're handling storage ourselvesrequest.urlfor fetch,reqURLfor cache β full URL to the server (preserves query params), pathname as cache key (strips volatile query params)res.clone()everywhere β going to cache AND returning from cache. A response can only be consumed once.respondWithnotwaitUntilβrespondWithintercepts 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
let url = URL(request.url); // β silent failure
let url = new URL(request.url); // β
correctDevTools Reset Ritual
When something isn't working, this is the full clean slate sequence:
- Go back online
- Stop the service worker
- Unregister it
- Clear cache storage
- Clear network log
- Refresh the page
Lecture 5: Offline Routing Q&A π
Why request.url for Fetch but reqURL for Cache?
res = await fetch(request.url, fetchOptions); // full URL β preserves query params
await cache.put(reqURL, res.clone()); // pathname only β stable cache keyQuery 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.