Skip to content
C Codeloom
Web

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.

·4 min read · By Codeloom
Intermediate 10 min read

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
Service worker lifecycle and fetch path

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.html will 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 in activate.
  • skipWaiting without 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.js you can deploy that calls self.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.