RoadmapDay 53 / 80
React (Web)Month 3 · Week 11

Day 53: PWA Mechanics & Service Workers

Understand the Service Worker lifecycle and caching strategies well enough to build a PWA that reliably works offline without serving stale content forever.

Mark this day complete

Study

Concepts

A Service Worker is a programmable network proxy, running separately from your page

A Service Worker is a JS file that runs in its own thread, separate from any page, and can intercept every network request the page makes via the `fetch` event — this is what makes offline support, custom caching strategies, and push notifications possible: the Service Worker decides whether to serve from cache, hit the network, or some combination, entirely programmatically. It has its own lifecycle independent of any open tab: `install` (typically used to pre-cache a set of assets — the "app shell"), `activate` (a good place to clean up old cache versions), and then it sits idle, waking up to handle `fetch`/`push`/`sync` events as they occur, even (for `push`) when no tab is open at all.

A new Service Worker version does not take over immediately — by default it installs in the background and stays "waiting" until all tabs using the OLD version are closed, to avoid a page having code that suddenly runs against an incompatible newly-cached asset mid-session. `self.skipWaiting()` (in the SW) and `clients.claim()` opt into taking over immediately, at the cost of a live tab occasionally seeing an update mid-session unexpectedly — a deliberate tradeoff, usually paired with a "New version available, refresh?" UI prompt.

Caching strategies: pick per resource type

Cache-first: check cache, fall back to network — best for immutable, versioned assets (hashed JS/CSS bundles) that never change once cached. Network-first: try network, fall back to cache on failure — best for frequently-changing data (an API response) where freshness matters more than speed, but offline availability is still valuable as a fallback. Stale-while-revalidate: serve the cached version INSTANTLY while simultaneously fetching a fresh copy in the background to update the cache for next time — a good default for most non-critical, semi-frequently-changing assets (a user avatar, a list of categories) where instant response matters more than perfect real-time freshness.

The Cache API (used directly, or via a helper library like Workbox which wraps these strategies as named recipes) is distinct from the browser's HTTP cache (Day 44) — it is entirely programmatic storage the Service Worker controls explicitly, keyed by request, not governed by Cache-Control headers.

See It

Visualizations

Visualization

Service Worker lifecycle

register()

Browser downloads and parses the SW script

install

Pre-cache the app shell

runs once per new SW version

waiting

New SW installed but NOT yet active

old SW still controls open tabs

activate

Old caches cleaned up, new SW takes control

after all old tabs close, or skipWaiting()

idle → fetch/push/sync

Wakes up per event, even with no tab open

Visualization

Caching strategy per resource type

 Cache-firstNetwork-first
Best forImmutable, hashed build assetsFrequently-changing API data
SpeedInstant (no network wait)As fast as the network allows
FreshnessOnly as fresh as cache-time (never re-checked)Always fresh when online
Offline behaviorWorks perfectlyFalls back to last cached response

Build It

Code Examples

A minimal Service Worker: pre-cache the app shell, serve cache-first

js
const CACHE_NAME = 'app-shell-v3'; // bump this string to invalidate old caches
const APP_SHELL = ['/', '/index.html', '/styles.css', '/app.bundle.js'];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL))
  );
});

self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
    )
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => cached ?? fetch(event.request))
  );
});

Stale-while-revalidate for a semi-frequently-changing resource

js
async function staleWhileRevalidate(request) {
  const cache = await caches.open('data-cache-v1');
  const cached = await cache.match(request);

  const networkFetch = fetch(request).then((response) => {
    cache.put(request, response.clone()); // update cache for NEXT time
    return response;
  });

  return cached ?? networkFetch; // instant if cached, otherwise wait for network
}

self.addEventListener('fetch', (event) => {
  if (event.request.url.includes('/api/categories')) {
    event.respondWith(staleWhileRevalidate(event.request));
  }
});

Remember

Key Takeaways

  • A Service Worker is a programmable proxy for every network request, running independent of any open tab.
  • install pre-caches the app shell; activate is the right place to clean up old cache versions by name.
  • A new SW stays "waiting" until old tabs close, unless you explicitly opt into skipWaiting()/clients.claim().
  • Cache-first for immutable assets, network-first for freshness-critical data, stale-while-revalidate as a good default balance.
  • The Cache API is explicit, programmatic storage the SW controls — distinct from the HTTP cache governed by Cache-Control headers (Day 44).

Do It

Practice

  1. 1Register the app-shell Service Worker example on a small static site, go offline in DevTools, and confirm the app still loads.
  2. 2Bump the CACHE_NAME, reload, and verify old caches are deleted on activate (check Application tab → Cache Storage).
  3. 3Implement stale-while-revalidate for one real API call and log both the instant cached response and the later network update.