Service Workers are the foundation of Progressive Web Apps — they run in a background thread, intercept network requests, enable offline functionality, and handle push notifications. Firefox has full Service Worker support. Here's a practical guide covering the lifecycle, caching strategies, and debugging in Firefox DevTools.
What Service Workers Actually Are
A Service Worker is a JavaScript file that:
- Runs in a separate thread from the page (Web Worker)
- Has no access to the DOM
- Intercepts and handles network requests for its scope
- Can cache resources and serve them offline
- Receives push notifications even when the page is closed
- Persists between page loads (unlike the page's JavaScript)
Browser support caveat: Firefox disables service workers in Private Browsing mode entirely. This is a Firefox-specific policy for privacy reasons.
The Service Worker Lifecycle
// Register in your page's JavaScript if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js', { scope: '/' }) .then(reg => console.log('SW registered:', reg.scope)) .catch(err => console.error('SW registration failed:', err)); } Lifecycle events in sw.js:
- install: fires when the SW is first registered or updated. Cache resources here.
- activate: fires after install, when the SW takes control. Clean up old caches here.
- fetch: fires for every network request in the SW's scope.
// sw.js const CACHE_NAME = 'app-v1'; const PRECACHE_URLS = ['/index.html', '/styles.css', '/app.js']; self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME).then(cache => cache.addAll(PRECACHE_URLS)) ); self.skipWaiting(); // Activate immediately without waiting }); self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(keys => Promise.all( keys.filter(key => key !== CACHE_NAME).map(key => caches.delete(key)) ) ) ); self.clients.claim(); // Take control of pages immediately }); Caching Strategies
Different strategies suit different resource types:
Cache First (for static assets with versioned URLs):
self.addEventListener('fetch', event => { event.respondWith( caches.match(event.request).then(cached => cached || fetch(event.request).then(response => { const clone = response.clone(); caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone)); return response; }) ) ); }); Network First (for API calls where fresh data matters):
event.respondWith( fetch(event.request) .then(response => { const clone = response.clone(); caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone)); return response; }) .catch(() => caches.match(event.request)) // fallback to cache ); Stale While Revalidate (serve cached, update in background):
event.respondWith( caches.open(CACHE_NAME).then(cache => cache.match(event.request).then(cached => { const fetchPromise = fetch(event.request).then(response => { cache.put(event.request, response.clone()); return response; }); return cached || fetchPromise; }) ) ); Firefox DevTools: Service Worker Panel
about:debugging → This Firefox → Service Workers
Shows all registered service workers, their status (installing/waiting/active), and scope. Buttons to:
- Inspect: opens DevTools connected to the service worker's thread
- Push: send a test push notification
- Unregister: remove the SW
For per-page SW inspection: DevTools → Application tab (if available) or about:debugging.
The Service Worker's own Console errors appear in the Browser Console (Ctrl+Shift+J) rather than the page DevTools console.
Cache Storage Inspection
DevTools → Storage → Cache Storage
Shows all named caches. Expand each cache to see:
- Cached URLs
- Response status codes
- Response headers
- Full response body
Right-click → Delete to remove individual cache entries. Useful for testing: delete a cached resource and reload to verify the network request fires.
Service Worker Debugging Workflow
The service worker lifecycle makes debugging tricky — a running SW from a previous session intercepts your requests even after you change sw.js. Use these patterns:
Bypass for testing: in DevTools → Network → check "Disable Cache" and use the SW bypass. Or navigate to about:debugging → unregister the SW.
Console in the SW: since SW has no DOM, console.log() in the SW appears in the Browser Console (Ctrl+Shift+J) with a label indicating it's from the service worker.
Inspect SW directly: in about:debugging → click Inspect → opens DevTools for the SW thread. You can set breakpoints in the SW code, inspect scope, and use the console with SW-level access.
Push Notifications
// In page JavaScript — subscribe to push const reg = await navigator.serviceWorker.ready; const subscription = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY) }); // Send subscription to your server await fetch('/api/subscribe', { method: 'POST', body: JSON.stringify(subscription) }); In the Service Worker — handle push events:
self.addEventListener('push', event => { const data = event.data?.json() || { title: 'New notification' }; event.waitUntil( self.registration.showNotification(data.title, { body: data.body, icon: '/icon-192.png', badge: '/badge-96.png', data: { url: data.url } }) ); }); self.addEventListener('notificationclick', event => { event.notification.close(); event.waitUntil( clients.openWindow(event.notification.data.url) ); }); Firefox permission: users must grant notification permission. The Permission API will show a doorhanger. In Settings → Notifications, users can manage per-site notification permissions.
Background Sync
// Queue a sync when offline navigator.serviceWorker.ready.then(reg => reg.sync.register('sync-messages') ); // In service worker self.addEventListener('sync', event => { if (event.tag === 'sync-messages') { event.waitUntil(syncPendingMessages()); } }); Background Sync fires when the device regains connectivity, even if the page is closed. Firefox supports Background Sync (behind a flag in some versions, enabled in Firefox 124+).
Workbox
Writing raw service worker code is complex. Workbox (by Google) is a library that provides pre-built caching strategies:
import { registerRoute } from 'workbox-routing'; import { StaleWhileRevalidate, CacheFirst } from 'workbox-strategies'; // Cache images for 30 days registerRoute( ({ request }) => request.destination === 'image', new CacheFirst({ cacheName: 'images', plugins: [ new ExpirationPlugin({ maxAgeSeconds: 30 * 24 * 60 * 60 }) ]}) ); // API routes: stale-while-revalidate registerRoute( ({ url }) => url.pathname.startsWith('/api/'), new StaleWhileRevalidate({ cacheName: 'api-cache' }) ); Workbox works in Firefox and generates production-ready SW code without writing the plumbing manually.