Service Workers Cheat Sheet
Covers registering a service worker, its install and activate lifecycle, caching strategies, and offline-first fetch handling.
Registering a Service Worker
Feature-detect and register from the main page.
// main.js — runs in the pageif ('serviceWorker' in navigator) { window.addEventListener('load', async () => { try { const reg = await navigator.serviceWorker.register('/sw.js', { scope: '/' }); console.log('SW registered:', reg.scope); } catch (err) { console.error('SW registration failed:', err); } });}
Install & Activate (Caching)
Pre-cache the app shell and clean up old caches.
// sw.jsconst CACHE_NAME = 'app-shell-v3';const ASSETS = ['/', '/index.html', '/styles.css', '/app.js'];self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS)) ); self.skipWaiting(); // activate the new SW immediately});self.addEventListener('activate', (event) => { event.waitUntil( caches.keys().then((keys) => Promise.all( keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key)) ) ) ); self.clients.claim(); // take control of open pages});
Fetch Event (Cache-First Strategy)
Serve from cache, fall back to network, then to an offline page.
self.addEventListener('fetch', (event) => { event.respondWith( caches.match(event.request).then((cached) => { if (cached) return cached; // cache-first return fetch(event.request) .then((response) => { const clone = response.clone(); caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone)); return response; }) .catch(() => caches.match('/offline.html')); }) );});
Lifecycle & Concepts
Key events and APIs every service worker uses.
- install- Fires once on registration; typically used to pre-cache the app shell
- activate- Fires when the SW takes control; used to clean up old caches
- fetch- Intercepts every network request from controlled pages, letting you serve from cache
- scope- A SW only controls pages under its registration path
- skipWaiting()- Forces a newly installed SW to activate without waiting for old tabs to close
- Background Sync- Defers actions (like form submits) until connectivity is restored
- Push API- Enables receiving push notifications even when the site tab is closed
Stale-While-Revalidate Strategy
Serve cached content instantly while updating the cache in the background for next time.
self.addEventListener('fetch', (event) => { if (event.request.method !== 'GET') return; event.respondWith( caches.open(CACHE_NAME).then(async (cache) => { const cached = await cache.match(event.request); const networkFetch = fetch(event.request) .then((response) => { if (response.ok) cache.put(event.request, response.clone()); return response; }) .catch(() => cached); // offline: fall back to whatever was cached // Return cached immediately if we have it; otherwise wait on the network return cached || networkFetch; }) );});
Background Sync for Offline Form Submits
Queue failed writes with IndexedDB and replay them once connectivity returns.
// Page: register a sync tag when a POST fails offlineasync function submitOrder(order) { try { await fetch('/api/orders', { method: 'POST', body: JSON.stringify(order) }); } catch { await saveToOutbox(order); // IndexedDB helper const reg = await navigator.serviceWorker.ready; await reg.sync.register('sync-orders'); }}// sw.js: replay queued requests when the browser regains connectivityself.addEventListener('sync', (event) => { if (event.tag === 'sync-orders') { event.waitUntil( readOutbox().then((orders) => Promise.all( orders.map((order) => fetch('/api/orders', { method: 'POST', body: JSON.stringify(order) }) .then(() => removeFromOutbox(order.id)) ) ) ) ); }});
Push API End-to-End
Subscribe on the client, then handle the incoming push event in the worker.
// Client: subscribe with a VAPID public key and send the subscription to your serverconst reg = await navigator.serviceWorker.ready;const subscription = await reg.pushManager.subscribe({ userVisibleOnly: true, // required: every push must show a notification applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),});await fetch('/api/subscribe', { method: 'POST', body: JSON.stringify(subscription) });// sw.js: react to a push message and show a notificationself.addEventListener('push', (event) => { const data = event.data ? event.data.json() : {}; event.waitUntil( self.registration.showNotification(data.title || 'Update', { body: data.body, icon: '/icon-192.png', data: { url: data.url }, }) );});self.addEventListener('notificationclick', (event) => { event.notification.close(); event.waitUntil(clients.openWindow(event.notification.data.url));});
Handling the Update Lifecycle Safely
Detect a waiting worker and prompt the user instead of silently swapping caches mid-session.
// Page: listen for a new SW that's installed but waiting (avoids skipWaiting surprises)navigator.serviceWorker.register('/sw.js').then((reg) => { reg.addEventListener('updatefound', () => { const newWorker = reg.installing; newWorker.addEventListener('statechange', () => { if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { // A new version is ready — show a 'Refresh to update' banner showUpdateBanner(() => { newWorker.postMessage({ type: 'SKIP_WAITING' }); }); } }); });});// Reload once the new SW actually takes control, exactly oncelet refreshing = false;navigator.serviceWorker.addEventListener('controllerchange', () => { if (refreshing) return; refreshing = true; window.location.reload();});// sw.js: listen for the page's message instead of calling skipWaiting() unconditionallyself.addEventListener('message', (event) => { if (event.data?.type === 'SKIP_WAITING') self.skipWaiting();});
Caching Strategies: When to Use Each
Picking the right strategy per resource type matters more than picking one for everything.
- Cache-first- Fastest, but can serve stale data forever; best for versioned/hashed static assets
- Network-first- Always fresh when online, falls back to cache offline; best for HTML/API responses that change often
- Stale-while-revalidate- Instant response + background refresh; best default for most GET requests
- Network-only- No caching at all; required for POST/PUT/analytics endpoints
- Cache-only- Never touches the network; used for pre-cached app-shell assets that never change per version
- Workbox- Google's library that codifies these strategies (staleWhileRevalidate(), networkFirst()) instead of hand-rolling fetch handlers
Service Workers require HTTPS (localhost is exempted for development) — always version your cache name (e.g. app-shell-v3) and delete stale caches in the activate handler, or users get stuck on old assets indefinitely.