Service Workers enable powerful offline experiences and background operations. Let's master them from registration to advanced caching strategies.
What are Service Workers?
Service Workers are JavaScript files that run independently of your web page, enabling:
- Offline functionality
- Background sync
- Push notifications
- Network request interception
- Custom caching strategies
Key Characteristics:
- Run in a separate thread
- Are event-driven
- Have no DOM access
- Use Promises heavily
- Can terminate when idle
Registration
Basic Registration
// Register Service Worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then((registration) => {
console.log('SW registered:', registration);
// Check for updates
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
if (newWorker) {
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// New version available
if (confirm('New version available. Reload?')) {
window.location.reload();
}
}
});
}
});
})
.catch((error) => {
console.error('SW registration failed:', error);
});
});
}Unregister Service Worker
navigator.serviceWorker.getRegistrations().then((registrations) => {
registrations.forEach((registration) => {
registration.unregister();
});
});Service Worker Lifecycle
Install Event
// sw.js
self.addEventListener('install', (event) => {
console.log('SW installing...');
// Cache static assets
event.waitUntil(
caches.open('static-v1').then((cache) => {
return cache.addAll([
'/',
'/index.html',
'/styles.css',
'/app.js',
'/offline.html'
]);
})
);
// Become active immediately
self.skipWaiting();
});Activate Event
self.addEventListener('activate', (event) => {
console.log('SW activating...');
// Clean up old caches
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((cacheName) => {
return cacheName !== 'static-v1' && cacheName !== 'dynamic-v1';
})
.map((cacheName) => {
return caches.delete(cacheName);
})
);
})
);
// Take control immediately
return self.clients.claim();
});Fetch Event
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
// Return cached version or fetch from network
return cachedResponse || fetch(event.request);
})
);
});Caching Strategies
Cache First - Fall Back to Network
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
return cachedResponse || fetch(event.request);
})
);
});Use for: Static assets (CSS, JS, images)
Network First - Fall Back to Cache
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.then((response) => {
// Clone response before caching
const responseClone = response.clone();
caches.open('dynamic-v1').then((cache) => {
cache.put(event.request, responseClone);
});
return response;
})
.catch(() => {
return caches.match(event.request);
})
);
});Use for: HTML, API requests
Cache Only
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}
// Return offline page if not cached
return caches.match('/offline.html');
})
);
});Use for: Offline-only assets
Network Only
self.addEventListener('fetch', (event) => {
event.respondWith(fetch(event.request));
});Use for: Non-cacheable requests (analytics, etc.)
Stale While Revalidate
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
const fetchPromise = fetch(event.request).then((networkResponse) => {
caches.open('dynamic-v1').then((cache) => {
cache.put(event.request, networkResponse.clone());
});
return networkResponse;
});
return cachedResponse || fetchPromise;
})
);
});Use for: Frequently updated content
Route-Based Strategies
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// Different strategies for different routes
if (url.pathname.startsWith('/api/')) {
// Network first for API
event.respondWith(networkFirst(event.request));
} else if (url.pathname.match(/\.(css|js|png|jpg)$/)) {
// Cache first for static assets
event.respondWith(cacheFirst(event.request));
} else {
// Stale while revalidate for pages
event.respondWith(staleWhileRevalidate(event.request));
}
});
function networkFirst(request) {
return fetch(request)
.then((response) => {
const clone = response.clone();
caches.open('api-v1').then((cache) => cache.put(request, clone));
return response;
})
.catch(() => caches.match(request));
}
function cacheFirst(request) {
return caches.match(request).then((cached) => {
return cached || fetch(request).then((response) => {
const clone = response.clone();
caches.open('static-v1').then((cache) => cache.put(request, clone));
return response;
});
});
}
function staleWhileRevalidate(request) {
return caches.match(request).then((cached) => {
const fetchPromise = fetch(request).then((response) => {
const clone = response.clone();
caches.open('pages-v1').then((cache) => cache.put(request, clone));
return response;
});
return cached || fetchPromise;
});
}Cache Management
Cache Versioning
const CACHE_VERSION = 'v2';
const CACHE_NAME = `my-app-${CACHE_VERSION}`;
const urlsToCache = [
'/',
'/index.html',
'/styles.css',
'/app.js'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
})
);
});Dynamic Cache Updates
// Cache API responses
self.addEventListener('fetch', (event) => {
if (event.request.url.startsWith('/api/')) {
event.respondWith(
fetch(event.request)
.then((response) => {
// Only cache successful responses
if (response.status === 200) {
const clone = response.clone();
caches.open('api-data').then((cache) => {
cache.put(event.request, clone);
});
}
return response;
})
.catch(() => {
return caches.match(event.request);
})
);
}
});Cache Expiration
// Implement cache expiration
const CACHE_EXPIRATION = {
'/api/': 5 * 60 * 1000, // 5 minutes
'/images/': 7 * 24 * 60 * 60 * 1000, // 7 days
'/static/': 30 * 24 * 60 * 60 * 1000 // 30 days
};
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
const cacheKey = getCacheKey(url.pathname);
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
if (cachedResponse) {
const cachedTime = parseInt(cachedResponse.headers.get('date') || '0');
const expiration = CACHE_EXPIRATION[cacheKey];
if (expiration && Date.now() - cachedTime > expiration) {
// Cache expired, fetch from network
return fetch(event.request).then((response) => {
const clone = response.clone();
clone.headers.set('date', Date.now().toString());
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, clone);
});
return response;
});
}
}
return cachedResponse || fetch(event.request);
})
);
});
function getCacheKey(pathname) {
if (pathname.startsWith('/api/')) return '/api/';
if (pathname.startsWith('/images/')) return '/images/';
if (pathname.startsWith('/static/')) return '/static/';
return null;
}Background Sync
Register Sync Event
// Request background sync
navigator.serviceWorker.ready.then((registration) => {
registration.sync.register('sync-messages');
});Handle Sync Event
// sw.js
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-messages') {
event.waitUntil(syncMessages());
}
});
async function syncMessages() {
try {
// Get unsent messages from IndexedDB
const messages = await getUnsentMessages();
// Send to server
await Promise.all(messages.map((message) => {
return fetch('/api/messages', {
method: 'POST',
body: JSON.stringify(message)
});
}));
// Clear IndexedDB
await clearUnsentMessages();
} catch (error) {
console.error('Sync failed:', error);
// Browser will retry automatically
}
}Push Notifications
Request Permission
async function requestNotificationPermission() {
const permission = await Notification.requestPermission();
if (permission === 'granted') {
console.log('Notification permission granted');
// Subscribe to push notifications
subscribeToPush();
}
}Subscribe to Push
async function subscribeToPush() {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array('YOUR_PUBLIC_KEY')
});
// Send subscription to server
await fetch('/api/push/subscribe', {
method: 'POST',
body: JSON.stringify(subscription)
});
}
function urlBase64ToUint8Array(base64String: string) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}Handle Push Event
// sw.js
self.addEventListener('push', (event) => {
const options = {
body: event.data ? event.data.text() : 'New message',
icon: '/icons/icon-192.png',
badge: '/icons/badge-72.png',
vibrate: [200, 100, 200],
data: {
dateOfArrival: Date.now(),
primaryKey: 1
}
};
event.waitUntil(
self.registration.showNotification('Push Notification', options)
);
});
// Handle notification click
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(
clients.openWindow('/')
);
});Offline Page
// sw.js
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.catch(() => {
// Return offline page when network fails
return caches.match('/offline.html');
})
);
});<!-- offline.html -->
<!DOCTYPE html>
<html>
<head>
<title>You're Offline</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
font-family: Arial, sans-serif;
text-align: center;
}
</style>
</head>
<body>
<div>
<h1>You're Offline</h1>
<p>Please check your internet connection.</p>
<button onclick="location.reload()">Try Again</button>
</div>
</body>
</html>Debugging
Chrome DevTools
- 1Application Tab
- Service Workers: View status, unregister, debug - Cache Storage: View cached resources - Clear Storage: Clear all data
- 1Network Tab
- Disable cache: Test Service Worker behavior - Offline mode: Test offline functionality
Logging
// sw.js
self.addEventListener('fetch', (event) => {
console.log('Fetching:', event.request.url);
event.respondWith(
caches.match(event.request).then((cached) => {
if (cached) {
console.log('From cache:', event.request.url);
return cached;
}
console.log('From network:', event.request.url);
return fetch(event.request);
})
);
});Best Practices
- 1Always Use HTTPS: Service Workers only work on secure contexts
- 2Version Your Caches: Prevent cache conflicts
- 3Clean Up Old Caches: Remove outdated cache versions
- 4Handle Errors Gracefully: Provide fallback content
- 5Test Offline Functionality: Regularly test offline behavior
- 6Update Service Worker: Notify users of updates
- 7Use Appropriate Strategies: Choose right caching strategy per resource type
- 8Monitor Performance: Track cache hit rates and performance
Performance Tips
- 1Preload Critical Resources: Cache important assets during install
- 2Use Cache Headers: Respect server cache headers
- 3Limit Cache Size: Prevent storage quota issues
- 4Compress Responses: Reduce bandwidth usage
- 5Cache API Responses: Reduce server load
Common Pitfalls
- 1Not Skipping Waiting: Users may see old content
- 2Not Handling Errors: Poor offline experience
- 3Over-caching: Wasted storage space
- 4Not Cleaning Up: Old caches taking space
- 5Ignoring Update Cycle: Users stuck on old versions
Conclusion
Service Workers enable powerful offline experiences and background operations. Master caching strategies, implement proper error handling, and always test thoroughly. The result is a faster, more resilient web application that works everywhere.