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
```javascript
// 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 is Go method: ‘POST’, body: JSON.stringify(item) }).then(() => removeFromIndexedDB(item.id)) )) ) ); } });
## Messaging Between Page and Service Worker
```javascript
// 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:
```javascript
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
```javascript
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):
```javascript
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((cached) => cached |
Comments