πŸš€AdvancedProgressive Web Apps

Service Worker Registration

Register a Service Worker from HTML, understand its lifecycle, and use it to enable offline capability and background sync.

Overview

A Service Worker is a JavaScript file that the browser runs in a separate background thread, acting as a programmable network proxy between your web app and the network. It can intercept fetch requests, serve responses from cache, push notifications, and run background sync all even when the browser tab is closed. Service Workers are registered from your main page JavaScript with `navigator.serviceWorker.register('/sw.js')`. The SW goes through a lifecycle: `installing` β†’ `installed` (waiting) β†’ `activating` β†’ `activated`. The first registration installs it; subsequent page loads may use the activated worker. A new version of sw.js waits until all tabs close before taking over. Service Workers require HTTPS (or localhost) and use the Fetch API and Cache Storage API to intercept network requests and serve offline-first responses.

Key Points

  • β€’Register with `navigator.serviceWorker.register('/sw.js', { scope: '/' })`
  • β€’The registration is a Promise; catch errors for HTTPS/file-system issues
  • β€’SW lifecycle: installing β†’ installed β†’ activating β†’ activated (waiting: blocks until all clients close)
  • β€’`skipWaiting()` + `clients.claim()` in the SW immediately activates a new version
  • β€’The `fetch` event in sw.js intercepts every network request from the SW's scope
  • β€’Cache API: `caches.open(name)`, `cache.put(req, res)`, `cache.match(req)` for storing responses
  • β€’SW does NOT have access to the DOM communicate via `postMessage` / `BroadcastChannel`

Examples

Registering a service worker from the HTML page

<!-- In your main HTML file's <script> -->
<script>
if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () => {
    try {
      const registration = await navigator.serviceWorker.register('/sw.js', {
        scope: '/'
      });

      console.log('SW registered, scope:', registration.scope);

      // Detect when a new SW version is waiting
      registration.addEventListener('updatefound', () => {
        const newWorker = registration.installing;
        newWorker.addEventListener('statechange', () => {
          if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
            // New version is waiting show update notification to user
            showUpdateBanner();
          }
        });
      });
    } catch (err) {
      console.error('SW registration failed:', err);
    }
  });
}
</script>

Always wrap registration in a "serviceWorker" in navigator check for browsers that don't support it.

Minimal service worker (sw.js) with cache-first strategy

// sw.js runs in background thread, not on the page

const CACHE_NAME = 'v1';
const PRECACHE_URLS = ['/', '/styles.css', '/app.js', '/offline.html'];

// Install: precache critical assets
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(PRECACHE_URLS))
      .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())
  );
});

// Fetch: cache-first, fall back to network
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then(cached => {
      return cached ?? fetch(event.request).then(response => {
        // Cache new successful responses
        if (response.ok) {
          const clone = response.clone();
          caches.open(CACHE_NAME).then(c => c.put(event.request, clone));
        }
        return response;
      }).catch(() => caches.match('/offline.html'))
    })
  );
});

This is a cache-first strategy: serve from cache, update cache in background, fall back to offline page.

Communicating between the page and service worker

// In the page send message to SW
navigator.serviceWorker.controller?.postMessage({
  type: 'SKIP_WAITING'
});

// In the page receive message from SW
navigator.serviceWorker.addEventListener('message', (event) => {
  if (event.data.type === 'CACHE_UPDATED') {
    console.log('New content available!');
  }
});

// In sw.js receive message from page
self.addEventListener('message', (event) => {
  if (event.data.type === 'SKIP_WAITING') {
    self.skipWaiting();
  }
  // Reply to the page
  event.source.postMessage({ type: 'CACHE_UPDATED' });
});

Common Mistakes

  • Registering the SW before the page has loaded use `window.addEventListener("load", ...)` to avoid competing for network during page load
  • Caching API responses or dynamic data with a simple cache-first strategy stale data causes bugs; use stale-while-revalidate or network-first for dynamic content
  • Forgetting `event.waitUntil()` in install/activate handlers without it, the browser may terminate the SW before async operations complete

Note

Use Workbox (by Google) for production service workers. It provides pre-built strategies (CacheFirst, NetworkFirst, StaleWhileRevalidate), precaching, and automatic cache versioning avoiding the need to write boilerplate cache management by hand.

Exercises

Write HTML to solve each exercise. Use Preview to see it render in a live iframe, then mark as done.

Loading editor…

Completed: 0 / 5 exercises

Knowledge Check

Question 1 of 4

What is the correct order of the Service Worker lifecycle states?