Service Workers are the backbone of Progressive Web Apps — they give websites offline support, push notifications, and background sync. Firefox has full Service Worker support, and the DevTools Application panel helps you debug them. This guide covers the registration lifecycle, caching strategies, and practical patterns.
What is a Service Worker?
A Service Worker is a JavaScript file that runs in a separate thread from the page, with no DOM access. It acts as a programmable network proxy — intercepting fetch requests, serving from cache, or falling back to the network.
Key capabilities:
- Intercept and respond to fetch requests
- Cache assets for offline use
- Receive push notifications (even when the tab is closed)
- Background sync (defer requests until online)
- Periodic background sync (update content in the background)
Registering a Service Worker
// In your main app code (index.js, main.js, etc.)
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js');
console.log('SW registered:', registration.scope);
} catch (err) {
console.error('SW registration failed:', err);
}
});
}
The scope defaults to the directory containing sw.js. A sw.js at /sw.js covers the entire origin. A sw.js at /app/sw.js only covers /app/ and below.
Service Worker Lifecycle
install → waiting → activate → fetch intercepts begin
The lifecycle in code:
// sw.js
const CACHE_NAME = 'my-app-v1';
const STATIC_ASSETS = [
'/',
'/index.html',
'/app.js',
'/app.css',
'/icons/icon-192.png',
];
// Install: cache static assets
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(STATIC_ASSETS))
.then(() => self.skipWaiting()) // Activate immediately
);
});
// Activate: clean up old caches
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys()
.then(keys => Promise.all(
keys
.filter(key => key !== CACHE_NAME)
.map(key => caches.delete(key))
))
.then(() => self.clients.claim()) // Take control of existing tabs
);
});
// Fetch: serve from cache, fall back to network
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then(cached => cached || fetch(event.request))
);
});
Caching Strategies
Cache First (for static assets)
Serve from cache, fall back to network. Best for versioned assets (JS, CSS with hashes):
async function cacheFirst(request) {
const cached = await caches.match(request);
if (cached) return cached;
const response = await fetch(request);
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
return response;
}
Network First (for API data)
Try network first, fall back to cache. Best for dynamic data that needs to be fresh:
async function networkFirst(request) {
try {
const response = await fetch(request);
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
return response;
} catch {
return caches.match(request);
}
}
Stale While Revalidate (for non-critical freshness)
Return cached version immediately, update cache in background:
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;
}
Combining Strategies
Route different request types to different strategies:
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// Cache-first for static assets
if (url.pathname.startsWith('/static/')) {
event.respondWith(cacheFirst(event.request));
return;
}
// Network-first for API calls
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(event.request));
return;
}
// Stale-while-revalidate for HTML pages
if (event.request.mode === 'navigate') {
event.respondWith(staleWhileRevalidate(event.request));
return;
}
});
Background Sync
Defer a request until the user is back online:
// In main thread
async function submitFormWithSync(data) {
try {
await fetch('/api/submit', { method: 'POST', body: JSON.stringify(data) });
} catch {
// Offline: store data and register sync
const db = await openDB();
await db.put('pending-submissions', data);
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('submit-form');
}
}
// In service worker
self.addEventListener('sync', async (event) => {
if (event.tag === 'submit-form') {
event.waitUntil(async () => {
const db = await openDB();
const items = await db.getAll('pending-submissions');
await Promise.all(items.map(item => fetch('/api/submit', {
method: 'POST', body: JSON.stringify(item)
})));
await db.clear('pending-submissions');
}());
}
});
Debugging Service Workers in Firefox
Firefox DevTools has dedicated Service Worker tools:
- Open DevTools → Storage tab → Service Workers section
- Or navigate to
about:debugging#/runtime/this-firefox
In the Storage inspector:
- See all registered service workers
- View their scope and status (installing, waiting, active)
- Click "Unregister" to remove a service worker
- Click "Push" to simulate a push notification
In the Network panel:
- Requests served from cache show "(service worker)" in the Transferred column
- You can bypass the service worker by holding Shift when reloading
Updating Service Workers
The biggest footgun: changes to sw.js don't take effect until the old service worker is replaced.
// In sw.js: change CACHE_NAME to force a new install
const CACHE_NAME = 'my-app-v2'; // Changed from v1
// In app: listen for update
const reg = await navigator.serviceWorker.register('/sw.js');
reg.addEventListener('updatefound', () => {
const newWorker = reg.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// New service worker available, show "Refresh to update" UI
showUpdateNotification();
}
});
});
Or use the skipWaiting() call in the install handler (shown above) to auto-activate, then clients.claim() to take over existing tabs immediately.
Workbox: Managed Service Workers
Google's Workbox library abstracts the boilerplate:
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
registerRoute(
/\.(?:js|css)$/,
new CacheFirst({
cacheName: 'static-resources',
plugins: [new ExpirationPlugin({ maxEntries: 50 })],
})
);
registerRoute(
/\/api\//,
new NetworkFirst({ cacheName: 'api-cache' })
);
Workbox is production-ready and handles edge cases like opaque responses and cache size limits.
Top comments (0)