Service Workers: A Deep Dive
How service workers actually run: lifecycle, fetch interception, caching strategies, updates, and the operational habits that keep them from breaking your site.
What you'll learn
- ✓What a service worker actually is
- ✓The install / activate / fetch lifecycle
- ✓Practical caching strategies
- ✓How updates roll out (and stall)
- ✓How to avoid bricking your site
Prerequisites
- •Comfort with JavaScript and the browser fetch API
Service workers are the most powerful and most dangerous primitive in the browser. They sit between every request and the network, run when your page is closed, and live across reloads. Used well, they make sites feel instant offline. Used badly, they ship a bug to every user that you cannot revert with a hotfix.
What and Why
A service worker is a JavaScript file the browser registers and runs in its own thread, separate from any page. It has no DOM access. Its job is to intercept network requests from pages under its scope, plus listen for events like push and sync. Browsers keep it around even after the page closes, terminate it when idle, and revive it on the next event.
You reach for service workers when you need true offline support, programmable caching beyond what HTTP gives you, background sync, web push, or to ship a PWA that survives flaky networks.
Mental Model
Think of the service worker as a programmable proxy that lives inside the browser. Every fetch from your pages is a packet that arrives at the worker first. The worker can answer from a cache, forward to the network, modify the request, or do both and race them. Nothing about the page knows or cares.
The lifecycle is the part that trips people up. There are three states that matter: installing, activated, and redundant. A new worker installs in the background while the old one keeps serving. It only takes over when every page using the old one closes, unless you tell it to skip waiting.
Hands-on Example
Register a worker from your page once.
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
A small worker that pre-caches the shell and serves it offline:
// sw.js
const CACHE = 'shell-v3';
const ASSETS = ['/', '/app.css', '/app.js', '/offline.html'];
self.addEventListener('install', (e) => {
e.waitUntil(caches.open(CACHE).then((c) => c.addAll(ASSETS)));
self.skipWaiting();
});
self.addEventListener('activate', (e) => {
e.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))
)
);
self.clients.claim();
});
self.addEventListener('fetch', (e) => {
const req = e.request;
if (req.mode === 'navigate') {
e.respondWith(fetch(req).catch(() => caches.match('/offline.html')));
return;
}
e.respondWith(caches.match(req).then((hit) => hit || fetch(req)));
});
register('/sw.js')
|
v
[installing] -- install event -> precache assets
|
v
[waiting] (old SW still controls pages)
|
skipWaiting() or all pages closed
v
[activated] -- activate event -> cleanup old caches
|
v
page fetch ----> [SW fetch handler] ----> respondWith
|
cache hit ------+------ network
\
offline fallback This pattern, “cache for static assets, network with fallback for navigations,” covers a huge share of real apps without inventing custom strategies.
Common Pitfalls
- Caching HTML forever. A stale
index.htmlwill hand users old asset URLs that no longer exist. Use network-first for navigations, cache-first for hashed assets. - Forgetting to bump the cache name. If you change asset contents but keep
shell-v1, users keep the old files. Version the cache name and clear old ones inactivate. skipWaitingwithout thinking. Activating a new worker mid-session can mix old page code with new asset URLs. Either coordinate a reload or wait for the next visit.- No kill switch. If a buggy worker ships, you cannot un-register it from the server. Keep a tiny
sw.jsyou can deploy that callsself.registration.unregister()and clears caches. - Treating it like a CDN. Service workers run on the user’s device. They speed up repeat visits, not first ones; pair with HTTP caching, not instead of it.
Practical Tips
Keep the worker small and obvious. Log a build hash inside it so you can confirm what is deployed. Use the Clear-Site-Data header as an emergency lever. Test in incognito and in throttled offline mode before every release. For real apps, lean on a library like Workbox for routing and strategies; hand-rolling is fine for learning, but you will reinvent the same bugs.
Treat the worker as production code with its own release notes. A bug here lasts as long as the cache.
Wrap-up
A service worker is a tiny programmable proxy with a long memory. Master the lifecycle, version your caches, plan an undo button, and you get genuinely offline-capable apps. Skip those steps and you ship a bug nobody can clear without devtools.
Related articles
- Web Progressive Web Apps: An Introduction
Learn what makes an app a PWA: service workers, manifests, install prompts, and offline-first strategies that turn a website into something that feels native.
- Web Progressive Web Apps (PWA): What They Are and When To Build One
A practical introduction to PWAs: manifests, service workers, offline caching, install prompts, and when a PWA is the right choice versus a native app.
- Web Content Security Policy Explained
A practical tour of CSP: what each directive does, how nonces and hashes work, how to roll out a policy safely with report-only mode, and the mistakes that quietly weaken it.
- Web Cookies vs localStorage vs sessionStorage
Compare the three main browser storage mechanisms by lifecycle, capacity, security, and use case so you pick the right tool for tokens, preferences, and state.