Service Worker Lifecycle: Install, Activate, Fetch

A Service Worker is a specialized JavaScript asset that runs in the background, acting as a programmable proxy between a web application, the browser, and the network. Understanding its lifecycle is essential for building resilient Progressive Web Apps (PWAs) with offline support and advanced caching strategies. This article breaks down how the Service Worker lifecycle operates and explores the core functional events: install, activate, and fetch.

The Service Worker Lifecycle Overview

Unlike standard JavaScript running in the main browser thread, a Service Worker operates independently of the web page lifecycle. It does not access the DOM directly and terminates when not in use.

The lifecycle follows a strict sequence: 1. Registration: The browser downloads and parses the Service Worker file. 2. Installation (install): The worker initializes and pre-caches critical application assets. 3. Activation (activate): The worker takes control and cleans up outdated caches from previous versions. 4. Operation / Idle (fetch, etc.): The worker listens for functional events like network requests or push notifications. 5. Redundant: The worker is replaced by a newer version or failed during installation.

Step 1: Registering the Service Worker

Before lifecycle events fire, the Service Worker must be registered from the main application script:

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

The install Event

The install event is the first event a Service Worker receives. It triggers once when the worker is first registered or when a byte-difference is detected in the Service Worker file compared to the currently active one.

Purpose

The primary role of the install event is to prepare the worker for use, typically by pre-caching static assets (HTML, CSS, JavaScript, icons) required to run the application offline.

Implementation

const CACHE_NAME = 'app-cache-v1';
const PRECACHE_ASSETS = [
  '/',
  '/index.html',
  '/styles.css',
  '/app.js'
];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then((cache) => {
        return cache.addAll(PRECACHE_ASSETS);
      })
      .then(() => {
        // Forces the waiting service worker to become the active service worker
        return self.skipWaiting();
      })
  );
});

The activate Event

Once installation completes successfully and no prior Service Worker is actively controlling open pages, the activate event fires.

Purpose

The activate event is primarily used for housekeeping tasks that cannot occur while the previous Service Worker version is still running, such as deleting obsolete caches and migrating indexedDB schemas.

Implementation

self.addEventListener('activate', (event) => {
  const cacheWhitelist = [CACHE_NAME];

  event.waitUntil(
    caches.keys()
      .then((cacheNames) => {
        return Promise.all(
          cacheNames.map((cacheName) => {
            if (!cacheWhitelist.includes(cacheName)) {
              return caches.delete(cacheName); // Delete old caches
            }
          })
        );
      })
      .then(() => {
        // Take immediate control of all open clients within scope
        return self.clients.claim();
      })
  );
});

The fetch Event

Once activated, the Service Worker enters the operational state and begins intercepting network requests via the fetch event.

Purpose

The fetch event intercepts every HTTP request made by pages within the Service Worker’s scope. Developers can inspect requests and decide whether to serve responses from the local cache, make a network request, or synthesize custom responses.

Implementation: Cache-First Strategy

In a “Cache-First” (or “Cache Falling Back to Network”) pattern, the worker checks the local cache first before attempting a network request:

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then((cachedResponse) => {
        // Return cached resource if found
        if (cachedResponse) {
          return cachedResponse;
        }

        // Otherwise, fetch from the network
        return fetch(event.request).then((networkResponse) => {
          // Check if we received a valid response
          if (!networkResponse || networkResponse.status !== 200 || networkResponse.type !== 'basic') {
            return networkResponse;
          }

          // Clone the response because it is a single-use stream
          const responseToCache = networkResponse.clone();

          caches.open(CACHE_NAME).then((cache) => {
            cache.put(event.request, responseToCache);
          });

          return networkResponse;
        });
      })
      .catch(() => {
        // Optional: Provide a fallback offline page for HTML requests
        if (event.request.headers.get('accept').includes('text/html')) {
          return caches.match('/offline.html');
        }
      })
  );
});