How Clients.claim Controls Active Service Worker Pages

The Clients.claim() method allows an active Service Worker to take immediate control of all open pages within its scope without requiring the user to reload the page. Under normal Service Worker lifecycle rules, a newly installed and activated worker only controls pages that are loaded after the activation completes. By invoking Clients.claim(), developers can override this default behavior, ensuring that in-progress network requests, caching rules, and message events are managed by the new Service Worker right away.

The Default Service Worker Lifecycle Problem

When a user visits a website that registers a Service Worker for the first time, the page starts loading before the Service Worker finishes installing and activating. Consequently, the initial page load is not controlled by the Service Worker, and navigator.serviceWorker.controller remains null.

Under the default lifecycle: 1. The Service Worker installs. 2. The Service Worker activates. 3. The open page continues running uncontrolled. 4. The Service Worker only takes control after the user navigates away or refreshes the page.

This behavior prevents race conditions where a page partially loaded without a Service Worker suddenly experiences intercepted network requests. However, for applications like Progressive Web Apps (PWAs) requiring immediate offline support or immediate messaging channels, waiting for a refresh is often undesirable.

How Clients.claim() Works

The Clients.claim() interface belongs to the ServiceWorkerGlobalScope (self.clients). When called, it scans the scope for all matching active client documents (such as browser tabs, iframes, or Web Workers) and associates them with the currently active Service Worker.

Implementation Example

Clients.claim() is typically called inside the activate event listener wrapped within event.waitUntil():

self.addEventListener('activate', (event) => {
  event.waitUntil(
    clients.claim()
  );
});

Because clients.claim() returns a Promise, passing it to event.waitUntil() ensures that the browser does not terminate or suspend the Service Worker until all active clients have been successfully claimed.

Pairing Clients.claim() with skipWaiting()

To achieve immediate control during an update cycle, Clients.claim() is almost always paired with self.skipWaiting() in the install event:

self.addEventListener('install', (event) => {
  // Forces the waiting service worker to become the active service worker
  self.skipWaiting();
});

self.addEventListener('activate', (event) => {
  // Immediately controls open client pages
  event.waitUntil(clients.claim());
});

Effects on the Client Page

Once Clients.claim() resolves, several immediate changes occur on the client side:

  1. Controller Property Updates: navigator.serviceWorker.controller on the web page changes from null (or the previous worker) to the new ServiceWorker instance.
  2. controllerchange Event Fires: The client-side page receives a controllerchange event on navigator.serviceWorker, allowing the JavaScript on the main thread to react (for example, by reloading resources or showing a notification):
navigator.serviceWorker.addEventListener('controllerchange', () => {
  console.log('A new Service Worker has taken control of this page.');
});
  1. Fetch Interception Begins: Any subsequent network requests triggered via fetch(), XMLHttpRequest, or HTML elements (like images and scripts) in the active tab are routed through the new worker’s fetch event listener.