DEV Community

Weather Clock Dash
Weather Clock Dash

Posted on

Service Workers: Caching Strategies, Background Sync, and Firefox DevTools Debugging

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);
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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))
  );
});
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
});
Enter fullscreen mode Exit fullscreen mode

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');
    }());
  }
});
Enter fullscreen mode Exit fullscreen mode

Debugging Service Workers in Firefox

Firefox DevTools has dedicated Service Worker tools:

  1. Open DevTools → Storage tab → Service Workers section
  2. 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();
    }
  });
});
Enter fullscreen mode Exit fullscreen mode

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' })
);
Enter fullscreen mode Exit fullscreen mode

Workbox is production-ready and handles edge cases like opaque responses and cache size limits.

Top comments (0)