Updating Service Workers
Updating service workers
Section titled “Updating service workers”Deploying a new version of your PWA means deploying a new service worker. Understanding how the browser detects and applies that update — and how to communicate it to users — prevents subtle bugs and stale-asset surprises.
How the browser detects updates
Section titled “How the browser detects updates”The browser checks your service worker file for changes on every page load (and whenever you call registration.update() manually). It compares the new download byte-for-byte against the currently installed SW. Even a single byte difference — a changed cache version string, a new comment, a whitespace tweak — triggers the update flow:
- The new SW is downloaded and parsed.
- The new SW enters the
installphase. - If installation succeeds, the new SW sits in Waiting — it cannot activate while the old SW still controls open tabs.
The waiting worker problem
Section titled “The waiting worker problem”The most common source of confusion in production PWAs:
- A user opens your app. The old SW activates and controls the tab.
- You deploy v2. The browser installs the new SW, but it sits in Waiting.
- The user keeps the tab open all day.
- The new SW never activates — the user never sees v2.
skipWaiting() — powerful but dangerous
Section titled “skipWaiting() — powerful but dangerous”Calling self.skipWaiting() inside the new SW’s install handler forces it to activate immediately, bypassing the wait:
self.addEventListener('install', (event) => { event.waitUntil( caches.open('v2').then((cache) => cache.addAll(['/', '/app.js', '/styles.css'])) .then(() => self.skipWaiting()) );});
self.addEventListener('activate', (event) => { event.waitUntil(self.clients.claim());});The danger: the old page is still running. If v2 has a different cache schema or asset URLs, the already-loaded page may request resources the new SW does not serve correctly. The result is a broken mid-session experience.
Recommendation: use skipWaiting() only when your asset URLs are versioned (e.g., app.abc123.js) so old and new assets are compatible, or always prompt the user to reload.
Prompting the user to reload
Section titled “Prompting the user to reload”The safest pattern: detect the waiting worker from the page and show a non-intrusive banner.
// main.js — page codeasync function registerSw() { if (!('serviceWorker' in navigator)) return;
const registration = await navigator.serviceWorker.register('/sw.js');
registration.addEventListener('updatefound', () => { const newWorker = registration.installing; if (!newWorker) return;
newWorker.addEventListener('statechange', () => { if ( newWorker.state === 'installed' && navigator.serviceWorker.controller ) { // A new SW is waiting — show update prompt showUpdateBanner(registration); } }); });}
function showUpdateBanner(registration) { const banner = document.createElement('div'); banner.textContent = 'A new version is available. '; const btn = document.createElement('button'); btn.textContent = 'Reload'; btn.onclick = () => { if (registration.waiting) { // Tell the waiting SW to skip waiting, then reload registration.waiting.postMessage({ type: 'SKIP_WAITING' }); } window.location.reload(); }; banner.appendChild(btn); document.body.prepend(banner);}
registerSw();The corresponding SW listens for that message:
// sw.js — service workerself.addEventListener('message', (event) => { if (event.data && event.data.type === 'SKIP_WAITING') { self.skipWaiting(); }});This keeps skipWaiting() entirely user-driven — the new version only activates when the user explicitly clicks Reload, preventing mid-session breakage.
registration.update() — proactive checking
Section titled “registration.update() — proactive checking”By default the browser checks for SW updates on navigation. For single-page apps where the user may never trigger a navigation, call registration.update() on an interval:
setInterval(() => registration.update(), 60 * 60 * 1000); // check every hourUpdate checklist
Section titled “Update checklist”| Step | Why |
|---|---|
Change at least one byte in sw.js | Triggers the browser’s byte-diff check |
Version your cache names (v2, v3) | Allows the activate handler to delete stale caches |
Delete old caches in activate | Avoids storing stale assets indefinitely |
Prompt users to reload instead of force-skipWaiting() | Prevents mid-session asset mismatches |
Call clients.claim() after skipWaiting() | Takes control of open pages immediately |