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();
})
);
});event.waitUntil(): Extends the lifetime of the install phase until the passed Promise resolves. If the Promise rejects, installation fails, and the worker is discarded.self.skipWaiting(): Skips the “waiting” state, allowing a newly installed Service Worker to activate immediately rather than waiting for all open tabs using the old worker to close.
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();
})
);
});self.clients.claim(): Allows the activated Service Worker to take immediate control of all uncontrolled open pages within its scope without requiring a page reload.
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');
}
})
);
});event.respondWith(): Hijacks the browser’s default networking behavior and provides a customPromiseresolving to aResponseobject.response.clone(): Duplicates the network response stream so one copy can be returned to the browser while the other is saved into the Cache Storage API.