How to implement Service Workers for offline caching in a PWA
Configure a Service Worker to cache assets and enable offline functionality for your Progressive Web App. This guide covers registration, caching strategies, and runtime updates.
Configure a Service Worker to cache assets and enable offline functionality for your Progressive Web App. These steps target Node 20.x and modern browsers supporting the standard API. You will create a registration script, define caching rules, and handle runtime updates.
Prerequisites
- A local development environment with Node.js 20.x installed.
- A static web application or a Laravel/React/Vue project.
- Access to the project root directory via terminal.
- A browser with Service Worker support enabled (Chrome, Firefox, Edge).
Step 1: Create the service-worker.js file
Create a new file named service-worker.js in the root of your project directory. This file acts as the intermediary between the browser and the network.
const CACHE_NAME = 'pwa-cache-v1';
const urlsToCache = [
'/',
'/index.html',
'/css/styles.css',
'/js/app.js',
'/images/logo.png'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
if (response) {
return response;
}
return fetch(event.request);
})
);
});
self.addEventListener('activate', event => {
const cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
})
);
});
Step 2: Register the Service Worker
Register the worker in your main JavaScript entry point, typically index.html or a global app.js. You must ensure the script is served over HTTPS or localhost, as Service Workers are restricted to secure contexts.
// In index.html or app.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
})
.catch(registrationError => {
console.log('ServiceWorker registration failed: ', registrationError);
});
});
}
Step 3: Implement a Cache First Strategy for Static Assets
Modify the fetch event listener to prioritize cached responses for static assets while falling back to the network for dynamic content. This ensures fast load times for images and stylesheets even when the network is unstable.
self.addEventListener('fetch', event => {
// Skip non-GET requests
if (event.request.method !== 'GET') {
return;
}
event.respondWith(
caches.match(event.request)
.then(response => {
if (response) {
return response;
}
return fetch(event.request).then(response => {
// Check if the response is a valid resource
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// Add to cache for 1 year (31536000 seconds)
const responseToCache = response.clone();
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, responseToCache);
});
return response;
});
})
);
});
Step 4: Handle Background Sync for Offline Forms
Implement background sync to queue requests when the user is offline and send them once connectivity is restored. This is essential for mobile apps that need to submit data without an active connection.
if ('serviceWorker' in navigator && 'Sync' in window) {
navigator.serviceWorker.ready.then(reg => {
reg.sync.register('sync-form-data');
});
}
Step 5: Update the Service Worker Dynamically
Ensure the browser detects changes to the Service Worker file. The browser automatically invalidates the old worker when the file hash changes, but you must trigger a re-registration if the worker is cached in the service worker cache storage.
// Optional: Force update on page reload if needed
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.getRegistrations().then(registrations => {
for (let registration of registrations) {
registration.unregister();
}
});
});
}
Verify the installation
Open your browser DevTools and navigate to the Application tab. Select Service Workers from the left sidebar. You will see the registered worker with its scope and status. Click the "Update" button if the status shows "Waiting" to install the new version immediately.
Run the following command in the browser console to check registration status:
console.log(navigator.serviceWorker.getRegistrations());
You will see an array containing the registration object with the scope and scriptURL.
Troubleshooting
Error: "Failed to register Service Worker: Not allowed to load script resource"
This occurs when the file is served over HTTP instead of HTTPS or localhost. Service Workers require a secure context. Ensure your development server uses HTTPS (e.g., https://localhost:443) or that you are running on localhost or 127.0.0.1.
Error: "ServiceWorker is not registered"
Check that the fetch event listener is included in the worker file. Without the fetch listener, the worker does nothing. Also, verify the file path matches the register() call exactly.
Issue: Caching only the first load
If new assets are not cached, ensure the fetch event listener is inside the install event or the fetch event is handling the response correctly. The code in Step 3 ensures new responses are cloned and put into the cache.
Issue: Old worker not updating
Clear the browser cache and hard reload the page (Ctrl+F5). If the issue persists, manually unregister the worker via the DevTools Application tab and reload the page to trigger a fresh registration.