Service Workers and Caching

· 5 min read · Updated March 16, 2026 · advanced
service-worker pwa caching offline web-api

Service Workers are a powerful browser API that sits between your web application and the network, acting as a programmable network proxy. They enable features like offline support, background sync, push notifications, and sophisticated caching strategies.

In this tutorial, you will learn how Service Workers work, how to register them, how to cache assets for offline use, and how to implement different caching strategies.

How Service Workers Fit Into the Web Platform

A Service Worker is a JavaScript file that runs separately from the main browser thread. It cannot access the DOM directly but can intercept network requests, manage caches, and communicate with open pages through the postMessage API.

The Service Worker lifecycle has three phases:

  1. Registration — You tell the browser about your Service Worker file
  2. Installation — The browser downloads and installs the worker
  3. Activation — The worker takes control of the page

Understanding this lifecycle is essential for debugging issues and implementing features like cache updates.

Registering a Service Worker

Before using a Service Worker, you must register it in your main JavaScript:

if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () => {
    try {
      const registration = await navigator.serviceWorker.register('/sw.js');
      console.log('Service Worker registered:', registration.scope);
    } catch (error) {
      console.error('Service Worker registration failed:', error);
    }
  });
}

The registration returns a ServiceWorkerRegistration object whose scope property indicates which pages the worker controls. The scope defaults to the directory containing the SW file and any subdirectories.

A few things to note:

  • The Service Worker file must be served from the same origin
  • You cannot register a Service Worker from a different domain
  • The file must be served over HTTPS (or localhost for development)

The Service Worker Lifecycle

Installation

When the browser finds a new Service Worker (or the file changes), it downloads the script and fires the install event:

// sw.js
const CACHE_NAME = 'my-cache-v1';
const ASSETS_TO_CACHE = [
  '/',
  '/index.html',
  '/styles.css',
  '/app.js',
  '/images/logo.png'
];

self.addEventListener('install', (event) => {
  console.log('Service Worker installing');
  
  // Pre-cache critical assets
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      console.log('Caching app assets');
      return cache.addAll(ASSETS_TO_CACHE);
    })
  );
  
  // Skip waiting to activate immediately
  self.skipWaiting();
});

The event.waitUntil() method extends the installation until the promise resolves. If the promise rejects, the installation fails and the Service Worker is discarded.

Calling self.skipWaiting() tells the browser to activate the worker immediately, even if there’s an existing controller.

Activation

After installation, the activate event fires. This is the perfect time to clean up old caches:

self.addEventListener('activate', (event) => {
  console.log('Service Worker activating');
  
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames.map((cacheName) => {
          // Delete caches that don't match the current version
          if (cacheName !== CACHE_NAME) {
            console.log('Deleting old cache:', cacheName);
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
  
  // Take control of all pages immediately
  self.clients.claim();
});

The self.clients.claim() method lets the Service Worker take control of existing pages immediately, rather than waiting for the next navigation.

Intercepting Network Requests

The core of Service Worker functionality is the fetch event, which lets you intercept network requests:

self.addEventListener('fetch', (event) => {
  // Handle requests here
  console.log('Intercepted request:', event.request.url);
});

From here, you can implement various caching strategies.

Caching Strategies

Cache First (Cache Fallback to Network)

This strategy checks the cache first, then falls back to the network:

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      // Return cached response if found
      if (response) {
        return response;
      }
      
      // Otherwise fetch from network
      return fetch(event.request).then((networkResponse) => {
        // Optionally cache the new response
        return networkResponse;
      });
    })
  );
});

This works well for static assets that rarely change.

Network First (Network Fallback to Cache)

For dynamic content that changes frequently:

self.addEventListener('fetch', (event) => {
  // Only handle GET requests
  if (event.request.method !== 'GET') return;
  
  event.respondWith(
    fetch(event.request)
      .then((response) => {
        // Clone and cache successful responses
        const responseClone = response.clone();
        caches.open(CACHE_NAME).then((cache) => {
          cache.put(event.request, responseClone);
        });
        return response;
      })
      .catch(() => {
        // If network fails, try the cache
        return caches.match(event.request);
      })
  );
});

This gives users the latest content when online, with offline fallback.

Stale-While-Revalidate

This strategy serves cached content immediately while updating the cache in the background:

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.match(event.request).then((cachedResponse) => {
        // Fetch and update cache in background
        const fetchPromise = fetch(event.request).then((networkResponse) => {
          cache.put(event.request, networkResponse.clone());
          return networkResponse;
        });
        
        // Return cached response immediately, or wait for network
        return cachedResponse || fetchPromise;
      });
    })
  );
});

This gives users instant responses while keeping content reasonably fresh.

Cache Versioning

When you update your app, you need to update the cache name to force the Service Worker to cache new versions:

// Increment this number when you update your app
const CACHE_VERSION = 2;
const CACHE_NAME = `my-app-v${CACHE_VERSION}`;

Alternatively, use a timestamp or hash:

const CACHE_NAME = `my-app-${Date.now()}`;

Then clean up old caches in the activation handler as shown earlier.

Communicating With the Service Worker

Pages can send messages to the Service Worker and receive responses:

// From the main page
const sw = navigator.serviceWorker.controller;

if (sw) {
  sw.postMessage({ type: 'SKIP_WAITING' });
}

// Listen for messages from the Service Worker
navigator.serviceWorker.addEventListener('message', (event) => {
  console.log('Message from SW:', event.data);
});
// In the Service Worker
self.addEventListener('message', (event) => {
  if (event.data.type === 'SKIP_WAITING') {
    self.skipWaiting();
  }
});

This pattern is useful for telling users to refresh when a new version is available.

Handling Background Sync

The Background Sync API lets you defer actions until the user has stable connectivity:

// In your main page
async function sendData(data) {
  if ('serviceWorker' in navigator && 'SyncManager' in window) {
    const registration = await navigator.serviceWorker.ready;
    await registration.sync.register('send-data');
    console.log('Sync registered');
  } else {
    // Fallback: send immediately
    await fetch('/api/data', { method: 'POST', body: JSON.stringify(data) });
  }
}
// In the Service Worker
self.addEventListener('sync', (event) => {
  if (event.tag === 'send-data') {
    event.waitUntil(syncData());
  }
});

async function syncData() {
  // Retrieve data from IndexedDB and send to server
}

This ensures data gets sent even if the user loses connection temporarily.

When to Use Service Workers

Service Workers are ideal for:

  • Progressive Web Apps — Offline functionality and installability
  • Asset caching — Faster page loads and offline support
  • Background processing — Background sync, push notifications
  • Network optimization — Advanced caching strategies

However, Service Workers add complexity. For simple websites that don’t need offline support, the extra overhead may not be worth it.

See Also