Service Workers and Offline Support
Building offline-capable web applications
Challenge
Section titled “Challenge”Enable offline functionality, cache assets intelligently, and handle network failures gracefully.
Explanation
Section titled “Explanation”Service Workers are scripts that run in the background, enabling offline support and background sync.
Basic Service Worker
Section titled “Basic Service Worker”// Register service workerif ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js') .then(reg => console.log('Service Worker registered')) .catch(err => console.error('Registration failed:', err));}
// sw.js - The service worker fileself.addEventListener('install', event => { console.log('Service Worker installing...');
event.waitUntil( caches.open('v1').then(cache => { return cache.addAll([ '/', '/index.html', '/styles.css', '/script.js' ]); }) );});
self.addEventListener('activate', event => { console.log('Service Worker activating...');
event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames.map(cacheName => { if (cacheName !== 'v1') { return caches.delete(cacheName); } }) ); }) );});
self.addEventListener('fetch', event => { event.respondWith( caches.match(event.request) .then(response => { if (response) { return response; // Cached }
return fetch(event.request) .then(response => { // Don't cache non-successful responses if (!response || response.status !== 200) { return response; }
// Clone and cache const responseClone = response.clone(); caches.open('v1').then(cache => { cache.put(event.request, responseClone); });
return response; }); }) .catch(() => { // Fallback for failed requests return new Response('Offline - resource not available'); }) );});Cache Strategies
Section titled “Cache Strategies”// Network Firstself.addEventListener('fetch', event => { event.respondWith( fetch(event.request) .then(response => { return caches.open('v1').then(cache => { cache.put(event.request, response.clone()); return response; }); }) .catch(() => { return caches.match(event.request); }) );});
// Stale While Revalidateself.addEventListener('fetch', event => { event.respondWith( caches.match(event.request) .then(cachedResponse => { const fetchPromise = fetch(event.request) .then(response => { caches.open('v1').then(cache => { cache.put(event.request, response.clone()); }); return response; });
return cachedResponse || fetchPromise; }) );});
// Cache Onlyself.addEventListener('fetch', event => { event.respondWith(caches.match(event.request));});Background Sync
Section titled “Background Sync”// Register background syncnavigator.serviceWorker.ready.then(swReg => { // Sync when online again return swReg.sync.register('sync-comments');});
// Handle sync event in SWself.addEventListener('sync', event => { if (event.tag === 'sync-comments') { event.waitUntil( syncComments() .catch(() => { // Retry sync return self.registration.sync.register('sync-comments'); }) ); }});
async function syncComments() { const db = await openDatabase('comments'); const pending = await db.getAll('pending');
for (const comment of pending) { const response = await fetch('/api/comments', { method: 'POST', body: JSON.stringify(comment) });
if (response.ok) { await db.delete('pending', comment.id); } }}Push Notifications
Section titled “Push Notifications”// Request notification permissionNotification.requestPermission().then(permission => { if (permission === 'granted') { navigator.serviceWorker.ready.then(swReg => { swReg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(publicKey) }); }); }});
// Handle push in service workerself.addEventListener('push', event => { const data = event.data?.json() || {};
event.waitUntil( self.registration.showNotification(data.title, { body: data.message, icon: '/icon.png', badge: '/badge.png' }) );});
// Handle notification clickself.addEventListener('notificationclick', event => { event.notification.close();
event.waitUntil( clients.matchAll({ type: 'window' }) .then(windowClients => { if (windowClients.length > 0) { windowClients[0].focus(); } else { clients.openWindow('/'); } }) );});Offline Storage
Section titled “Offline Storage”// IndexedDB for structured dataconst dbRequest = indexedDB.open('myApp', 1);
dbRequest.onupgradeneeded = event => { const db = event.target.result; const store = db.createObjectStore('tasks', { keyPath: 'id' }); store.createIndex('status', 'status', { unique: false });};
dbRequest.onsuccess = event => { const db = event.target.result; const tx = db.transaction('tasks', 'readwrite'); const store = tx.objectStore('tasks');
// Add task store.add({ id: 1, title: 'Task', status: 'pending' });
// Query const getRequest = store.get(1); getRequest.onsuccess = () => { console.log(getRequest.result); };};
// LocalStorage for simple datalocalStorage.setItem('offline-queue', JSON.stringify(queue));