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:

  1. install: fires when the SW is first registered or updated. Cache resources here.
  2. activate: fires after install, when the SW takes control. Clean up old caches here.
  3. 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.