Introduction
app and the network — enabling offline support, background sync, push notifications, and performance improvements through caching.
What Service Workers Can Do
- Offline support — serve cached content when the network is unavailable
- Cache management — control exactly what gets cached and for how long
- Background sync — defer actions until connectivity is restored
- Push notifications — receive messages even when the app isn’t open
- Performance — serve assets from cache instead of the network
Service Worker Lifecycle
Register → Download → Install → Activate → Idle → Fetch/Message/Push
er** — your page registers the SW file
2. Download — browser downloads the SW script
3. Install — SW installs, install event fires (cache assets here)
4. Waiting — new SW waits for old SW to release clients
5. Activate — SW activates, activate event fires (clean old caches here)
6. Idle — SW waits for events
7. Terminated — browser may terminate idle SWs to save memory
Registering a Service Worker
// main.js — register in your main page script
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/' // controls which pages the SW manages
});
console.log('SW registered, scope:', registration.scope);
// Listen for updates
registration.addEventListener('updatefound', () => {
const newSW = registration.installing;
e', () => {
console.log('SW state:', newSW.state);
// states: installing → installed → activating → activated
});
});
} catch (error) {
console.error('SW registration failed:', error);
}
});
}
Scope
The scope determines which pages the SW controls. /sw.js at the root controls all pages. /posts/sw.js only controls pages under /posts/:
// Only controls /posts/* pages
navigatviceWorker.register('/posts/sw.js', { scope: '/posts/' });
The Install Event: Pre-caching
Cache critical assets during installation:
// sw.js
const CACHE_NAME = 'myapp-v1';
const PRECACHE_ASSETS = [
'/',
'/index.html',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.png',
'/offline.html'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
co assets');
return cache.addAll(PRECACHE_ASSETS);
})
.then(() => self.skipWaiting()) // activate immediately
);
});
event.waitUntil() keeps the SW alive until the promise resolves. self.skipWaiting() forces the new SW to activate without waiting for old clients to close.
The Activate Event: Cache Cleanup
Remove old caches when a new SW activates:
self.addEventListener('activate', (event) => {
const CURRENT_CACHES = [CACHE_NAME];
t.waitUntil(
caches.keys()
.then((cacheNames) =>
Promise.all(
cacheNames
.filter((name) => !CURRENT_CACHES.includes(name))
.map((name) => {
console.log('Deleting old cache:', name);
return caches.delete(name);
})
)
)
.then(() => self.clients.claim()) // take control of all clients
);
});
self.clients.claim() makes the SW take control of all open pages immediately.
The Fetch Event: Caching Strategies
The fetch event intercepts all network requests. Choose a strategy based on the resource type:
Strategy 1: Cache First (Static Assets)
| fetch(event.request))event.s
- MDN: Service Worker API
- web.dev: Service Workers
- Workbox Documentation
- Service Worker Cookbook ‘pages’ }) );
## Debugging Service Workers
### Chrome DevTools
1. DevTools → Application → Service Workers
2. Check "Update on reload" during development
3. Click "Unregister" to remove the SW
4. Use "Bypass for network" to skip the SW temporarily
### Inspecting Caches
// In DevTools console const cacheNames = await caches.keys(); console.log(cacheNames);
const cache = await caches.open(‘myapp-v1’); const keys = await cache.keys(); keys.forEach(req => console.log(req.url));
## Resource// Cache images for 30 days
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images',
plugins: [new ExpirationPlugin({ maxAgeSeconds: 30 * 24 * 60 * 60 })]
})
);
// Network first for API
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new NetworkFirst({ cacheName: 'api-cache' })
);
// Stale while revalidate for pages
registerRoute(
({ request }) => request.mode === 'navigate',
new StaleWhileRevalidate({ cacheName:ogle's library that simplifies service worker development:
npm install workbox-webpack-plugin
or for Vite:
npm install vite-plugin-pwa
// sw.js with Workbox import { precacheAndRoute } from ‘workbox-precaching’; import { registerRoute } from ‘workbox-routing’; import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from ‘workbox-strategies’; import { ExpirationPlugin } from ‘workbox-expiration’;
// Pre-cache assets (injected by build tool) precacheAndRoute(self.__WB_MANIFEST);
ATED’ }); }); });
// Service Worker → Specific Client (the sender) self.addEventListener(‘message’, (event) => { if (event.data.type === ‘PING’) { event.source.postMessage({ type: ‘PONG’ }); } });
// Page listening for SW messages navigator.serviceWorker.addEventListener(‘message’, (event) => { if (event.data.type === ‘CACHE_UPDATED’) { showUpdateBanner(); } });
## Using Workbox (Recommended for Production)
[Workbox](https://developer.chrome.com/docs/workbox/) is Go method: 'POST',
body: JSON.stringify(item)
}).then(() => removeFromIndexedDB(item.id))
))
)
);
}
});
Messaging Between Page and Service Worker
// Page → Service Worker
navigator.serviceWorker.controller?.postMessage({
type: 'SKIP_WAITING'
});
// Service Worker → All Clients
self.clients.matchAll().then((clients) => {
clients.forEach((client) => {
client.postMessage({ type: 'CACHE_UPDgify(data) });
} catch {
// Store for later and register sync
await saveToIndexedDB(data);
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('submit-form');
}
}
// In sw.js
self.addEventListener('sync', (event) => {
if (event.tag === 'submit-form') {
event.waitUntil(
getFromIndexedDB().then((items) =>
Promise.all(items.map((item) =>
fetch('/api/submit', {
e when the network is unavailable:
self.addEventListener(‘fetch’, (event) => { if (event.request.headers.get(‘accept’)?.includes(’text/html’)) { event.respondWith( fetch(event.request) .catch(() => caches.match(’/offline.html’)) ); } });
## Background Sync
Defer actions until connectivity is restored:
// In your page async function submitForm(data) { try { await fetch(’/api/submit’, { method: ‘POST’, body: JSON.strinME); cache.put(request, response.clone()); return response; } catch { return caches.match(request); } }
async function staleWhileRevalidate(request) { const cache = await caches.open(CACHE_NAME); const cached = await cache.match(request); const networkPromise = fetch(request).then((response) => { cache.put(request, response.clone()); return response; }); return cached || networkPromise; }
## Offline Fallback Page
Show a custom offline pag
if (event.request.headers.get('accept')?.includes('text/html')) {
event.respondWith(staleWhileRevalidate(event.request));
return;
}
// Default: network first
event.respondWith(networkFirst(event.request));
});
async function cacheFirst(request) {
const cached = await caches.match(request);
return cached || fetch(request);
}
async function networkFirst(request) {
try {
const response = await fetch(request);
const cache = await caches.open(CACHE_NA);
});
Complete Strategy Router
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// Static assets: cache first
if (url.pathname.match(/\.(css|js|png|jpg|svg|woff2)$/)) {
event.respondWith(cacheFirst(event.request));
return;
}
// API calls: network first
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(event.request));
return;
}
// HTML pages: stale while revalidate const networkFetch = fetch(event.request).then((response) => {
cache.put(event.request, response.clone());
return response;
});
return cached || networkFetch; // return cache immediately if available
})
)
);
});
Strategy 4: Cache Only (Offline-First)
Only serve from cache — never hit the network:
self.addEventListener('fetch', (event) => {
event.respondWith(caches.match(event.request)request, clone));
return response;
})
.catch(() => caches.match(event.request)) // fall back to cache
);
}
});
Strategy 3: Stale While Revalidate (Best for Most Content)
Serve from cache immediately, update cache in background:
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open(CACHE_NAME).then((cache) =>
cache.match(event.request).then((cached) => {
);
});
Strategy 2: Network First (API Calls)
Try network, fall back to cache. Best for dynamic data:
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/api/')) {
event.respondWith(
fetch(event.request)
.then((response) => {
// Cache the fresh response
const clone = response.clone();
caches.open(CACHE_NAME).then((cache) => cache.put(
Serve from cache, fall back to network. Best for versioned assets (CSS, JS, images):
self.addEventListener(‘fetch’, (event) => { event.respondWith( caches.match(event.request) .then((cached) => cached |
Comments