Getting started
Service Workers act as network proxies, intercepting requests and serving cached responses for offline-first web apps.
Registration
// Check support & register
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js");
}
With scope
// Register with custom scope
navigator.serviceWorker.register("/sw.js", {
scope: "/app/",
});
// Returns: Promise<ServiceWorkerRegistration>
Registration promise
navigator.serviceWorker
.register("/sw.js")
.then((reg) => {
console.log("Registered:", reg.scope);
})
.catch((error) => {
console.error("Failed:", error);
});
Lifecycle events
Install event
const CACHE = "v1";
self.addEventListener("install", (event) => {
event.waitUntil(
caches
.open(CACHE)
.then((cache) =>
cache.addAll(["/", "/index.html", "/style.css", "/app.js"]),
),
);
});
Pre-cache critical assets during installation.
Activate event
self.addEventListener("activate", (event) => {
const keep = ["v2"];
event.waitUntil(
caches
.keys()
.then((keys) =>
Promise.all(
keys.filter((k) => !keep.includes(k)).map((k) => caches.delete(k)),
),
),
);
});
Clean up old caches on activation.
Fetch event
self.addEventListener("fetch", (event) => {
event.respondWith(
caches
.match(event.request)
.then((cached) => cached || fetch(event.request)),
);
});
Intercept network requests.
Message event
self.addEventListener("message", (event) => {
console.log("Message:", event.data);
event.source.postMessage("Reply");
});
Communicate with client pages.
Caching strategies
Cache first
self.addEventListener("fetch", (event) => {
event.respondWith(
caches
.match(event.request)
.then((cached) => cached || fetch(event.request)),
);
});
Serve from cache, fallback to network.
Network first
self.addEventListener("fetch", (event) => {
event.respondWith(
fetch(event.request).catch(() => caches.match(event.request)),
);
});
Try network first, fallback to cache.
Stale while revalidate
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.open(CACHE).then((cache) =>
cache.match(event.request).then((cached) => {
const fetched = fetch(event.request).then((response) => {
cache.put(event.request, response.clone());
return response;
});
return cached || fetched;
}),
),
);
});
Serve cached, update in background.
Cache only
self.addEventListener("fetch", (event) => {
event.respondWith(caches.match(event.request));
});
Only serve from cache.
Network only
self.addEventListener("fetch", (event) => {
event.respondWith(fetch(event.request));
});
Always fetch from network.
Cache API
Opening caches
caches.open(name);
// Returns: Promise<Cache>
Deleting caches
caches.delete(name);
// Returns: Promise<boolean>
Listing caches
caches.keys();
// Returns: Promise<string[]>
Adding to cache
cache.add(request);
// Returns: Promise<void>
Adding multiple
cache.addAll(["/index.html", "/style.css", "/app.js"]);
// Returns: Promise<void>
Storing response
cache.put(request, response);
// Returns: Promise<void>
Matching request
cache.match(request);
// Returns: Promise<Response|undefined>
Matching all
cache.matchAll(request);
// Returns: Promise<Response[]>
Deleting entry
cache.delete(request);
// Returns: Promise<boolean>
Listing entries
cache.keys();
// Returns: Promise<Request[]>
Skip waiting & claiming
Skip waiting
// In service worker
self.addEventListener("install", (event) => {
self.skipWaiting();
});
Activate immediately without waiting.
Claim clients
self.addEventListener("activate", (event) => {
event.waitUntil(self.clients.claim());
});
Take control of all open pages.
Combined pattern
self.addEventListener("install", (event) => {
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil(clients.claim());
});
Force immediate activation and control.
Clients API
Get all clients
self.clients.matchAll(options)
// Returns: Promise<Client[]>
// Options
{
includeUncontrolled: false,
type: 'window' // 'window', 'worker', 'all'
}
Get client by ID
self.clients.get(id);
// Returns: Promise<Client>
Open new window
self.clients.openWindow(url);
// Returns: Promise<WindowClient>
Claim clients
self.clients.claim();
// Returns: Promise<void>
Send message to client
client.postMessage(message);
Client properties
client.id; // string
client.type; // 'window' | 'worker'
client.url; // string
client.frameType; // 'top-level' | 'nested'
Push notifications
Subscribe from page
const reg = await navigator.serviceWorker.ready;
const sub = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: vapidPublicKey,
});
// Send subscription to server
await fetch("/subscribe", {
method: "POST",
body: JSON.stringify(sub),
headers: { "Content-Type": "application/json" },
});
Subscribe to push notifications.
Listen in service worker
self.addEventListener("push", (event) => {
const data = event.data.json();
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: data.icon,
badge: data.badge,
tag: data.tag,
data: data.url,
}),
);
});
Handle incoming push events.
Notification click
self.addEventListener("notificationclick", (event) => {
event.notification.close();
event.waitUntil(clients.openWindow(event.notification.data));
});
Handle notification clicks.
Unsubscribe
const reg = await navigator.serviceWorker.ready;
const sub = await reg.pushManager.getSubscription();
if (sub) {
await sub.unsubscribe();
}
Unsubscribe from push notifications.
Background sync
Register sync from page
const reg = await navigator.serviceWorker.ready;
await reg.sync.register("sync-messages");
Register background sync task.
Listen in service worker
self.addEventListener("sync", (event) => {
if (event.tag === "sync-messages") {
event.waitUntil(sendOutboxMessages());
}
});
Handle sync events.
Example sync function
async function sendOutboxMessages() {
const outbox = await getOutboxMessages();
await Promise.all(
outbox.map((msg) =>
fetch("/api/messages", {
method: "POST",
body: JSON.stringify(msg),
}).then(() => removeFromOutbox(msg.id)),
),
);
}
Process queued tasks.
ServiceWorkerRegistration
Properties
reg.installing; // ServiceWorker | null
reg.waiting; // ServiceWorker | null
reg.active; // ServiceWorker | null
reg.scope; // string
Update registration
reg.update();
// Returns: Promise<void>
Unregister
reg.unregister();
// Returns: Promise<boolean>
Push manager
reg.pushManager;
// PushManager instance
Background sync
reg.sync;
// SyncManager instance
Notifications
reg.showNotification(title, options);
reg.getNotifications(options);
Communication patterns
Page to service worker
// From page
navigator.serviceWorker.controller.postMessage({
type: "CACHE_URLS",
urls: ["/page1.html", "/page2.html"],
});
Send messages from page.
Service worker to page
// In service worker
self.addEventListener("message", (event) => {
event.source.postMessage({
type: "CACHE_COMPLETE",
});
});
Reply to page.
Broadcast to all clients
self.clients.matchAll().then((clients) => {
clients.forEach((client) =>
client.postMessage({
type: "UPDATE_AVAILABLE",
}),
);
});
Send to all open pages.
Page listens for messages
navigator.serviceWorker.addEventListener("message", (event) => {
if (event.data.type === "UPDATE_AVAILABLE") {
showUpdatePrompt();
}
});
Receive messages in page.
Advanced caching
Cache with expiration
const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
async function cachedFetch(request) {
const cache = await caches.open(CACHE);
const cached = await cache.match(request);
if (cached) {
const cachedTime = new Date(cached.headers.get("date"));
const age = Date.now() - cachedTime.getTime();
if (age < CACHE_DURATION) {
return cached;
}
}
const response = await fetch(request);
cache.put(request, response.clone());
return response;
}
Cache with time-based expiration.
Selective caching
self.addEventListener("fetch", (event) => {
const url = new URL(event.request.url);
// Only cache same-origin requests
if (url.origin !== location.origin) {
event.respondWith(fetch(event.request));
return;
}
// Only cache GET requests
if (event.request.method !== "GET") {
event.respondWith(fetch(event.request));
return;
}
event.respondWith(
caches
.match(event.request)
.then((cached) => cached || fetch(event.request)),
);
});
Cache only specific requests.
Cache size limits
async function trimCache(cacheName, maxItems) {
const cache = await caches.open(cacheName);
const keys = await cache.keys();
if (keys.length > maxItems) {
await cache.delete(keys[0]);
return trimCache(cacheName, maxItems);
}
}
Limit cache entry count.
Debugging
Chrome DevTools
Application → Service Workers
- View status (activated/waiting/installing)
- Update on reload
- Bypass for network
- Unregister
Application → Cache Storage
- Inspect cached entries
- Delete caches
Chrome debugging features.
Firefox DevTools
Application → Service Workers
- View registrations
- Start/stop workers
- Unregister
Storage → Cache Storage
- Browse caches
- Delete entries
Firefox debugging features.
Force update
navigator.serviceWorker.getRegistration().then((reg) => reg.update());
Manually check for updates.
Unregister all
navigator.serviceWorker
.getRegistrations()
.then((regs) => Promise.all(regs.map((reg) => reg.unregister())));
Remove all service workers.
Gotchas & limitations
HTTPS requirement
Service Workers require HTTPS in production (localhost exempt for development).
Scope limitations
Scope is determined by file location. /scripts/sw.js can only control /scripts/*. To control /, place at root or use Service-Worker-Allowed header.
Waiting period
New service worker waits until old one no longer controls clients. Use skipWaiting() + clients.claim() for immediate activation.
No DOM access
Service workers run in worker context. Use postMessage() to communicate with pages.
Opaque responses
Cross-origin requests without CORS create opaque responses. These inflate cache size (may report 7MB for 1KB file).
Cache persistence
Old caches persist unless explicitly deleted in activate event. Always clean up outdated caches.
Update checks
Browser checks for updated service worker on navigation. Force check with registration.update().
Installation failure
If any file in cache.addAll() fails to fetch, entire installation fails. Handle errors appropriately.
Also see
- MDN: Service Worker API (developer.mozilla.org)
- MDN: Using Service Workers (developer.mozilla.org)
- web.dev: Service Workers (web.dev)
- MDN: Cache API (developer.mozilla.org)
- web.dev: Caching Strategies (web.dev)
- MDN: Push API (developer.mozilla.org)
- MDN: Background Sync API (developer.mozilla.org)