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());
});self.skipWaiting(): Moves the newly installed worker out of thewaitingphase directly into theactivatingphase, replacing the old worker without waiting for existing tabs to close.clients.claim(): Forces all currently running pages to start using this new active worker immediately.
Effects on the Client Page
Once Clients.claim() resolves, several immediate changes
occur on the client side:
- Controller Property Updates:
navigator.serviceWorker.controlleron the web page changes fromnull(or the previous worker) to the newServiceWorkerinstance. controllerchangeEvent Fires: The client-side page receives acontrollerchangeevent onnavigator.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.');
});- 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’sfetchevent listener.