Introduction to Service Workers
In our previous lecture, we explored the principles of Progressive Web Apps and their core components. Today, we'll take a deep dive into one of the most crucial elements: Service Workers. Service Workers are the technology that enables many of the most powerful PWA features, including offline functionality, background processing, and push notifications.
Service Workers act as network proxies that sit between your web application and the network, allowing you to intercept and modify network requests, cache resources, and implement complex caching strategies. This powerful capability fundamentally changes how we think about web application architecture and reliability.
Analogy: The Butler of Your Web App
Think of a Service Worker as a dedicated butler for your web application. This butler stands at the doorway between your app and the outside world (the network). When your app needs something, instead of fetching it directly, the butler intercepts the request. The butler can:
- Check if the requested item is already in the pantry (cache)
- Go out to get fresh items when needed (network requests)
- Store new items for future use (caching)
- Serve alternative items when the store is closed (offline fallbacks)
- Organize the pantry when not busy (cache management)
- Receive messages even when you're not home (push notifications)
Like a good butler, the Service Worker works independently of the main house activities (main thread), ensuring the household keeps running smoothly regardless of external conditions.
Key Characteristics of Service Workers
Independent JavaScript Thread
Service Workers run in a separate thread from the main browser thread, allowing them to operate independently of the web page. This separation means they can:
- Continue running even when the user leaves the page
- Function without blocking the main UI thread
- Persist between page loads until explicitly terminated
graph TD
A[Browser] --> B[Main Thread: UI/Rendering]
A --> C[Service Worker Thread]
B <-.->|Message Passing| C
B -->|Network Requests| D[Network]
C -->|Intercepts| B-->D
C -->|Cache Interaction| E[Cache Storage]
JavaScript Promises-Based
Service Workers heavily use JavaScript Promises to handle asynchronous operations. Nearly all Service Worker APIs return Promises, making them well-suited for modern asynchronous JavaScript patterns like async/await.
Security Restrictions
Service Workers have significant security restrictions:
- HTTPS Only: Service Workers can only be registered on sites served over HTTPS (with an exception for localhost during development)
- Same-Origin Policy: Service Workers can only control pages from the same origin
- Scope Restrictions: Service Workers are generally limited to controlling pages within their registered scope (typically the directory where the Service Worker file is located)
Lifecycle Management
Service Workers have a well-defined lifecycle that includes installation, activation, and potential termination. Understanding this lifecycle is crucial for proper implementation.
graph TD
A[Register] --> B[Installing]
B -->|install event| C[Installed/Waiting]
C -->|activate event| D[Activating]
D --> E[Activated]
E --> F[Idle]
F --> G[Terminated]
F --> H[Fetch Event]
H --> F
F --> I[Push Event]
I --> F
F --> J[Sync Event]
J --> F
G -.-> A
Event-Driven
Service Workers operate on an event-driven model, responding to events like:
- install: When the Service Worker is being installed
- activate: When the Service Worker becomes active
- fetch: When the browser makes a network request
- push: When a push notification arrives
- sync: When background synchronization is triggered
- message: When messages are sent to the Service Worker
Service Worker Lifecycle
Registration
The Service Worker lifecycle begins with registration, which tells the browser where the Service Worker JavaScript file is located. This is typically done in your main application JavaScript:
Service Worker Registration
// Check if the browser supports Service Workers
if ('serviceWorker' in navigator) {
// Wait for the page to load
window.addEventListener('load', function() {
// Register the Service Worker
navigator.serviceWorker.register('/service-worker.js')
.then(function(registration) {
// Registration was successful
console.log('ServiceWorker registration successful with scope: ',
registration.scope);
})
.catch(function(error) {
// Registration failed
console.error('ServiceWorker registration failed: ', error);
});
});
}
You can customize the registration by specifying options:
Service Worker Registration with Options
navigator.serviceWorker.register('/service-worker.js', {
// Set the scope (defaults to the path where the SW file is located)
scope: '/app/',
// Type of Service Worker: 'classic' or 'module'
type: 'module',
// Update on page load: automatically update the service worker when a new page load occurs
updateViaCache: 'none' // 'imports', 'all', or 'none'
})
Installation
After registration, the browser attempts to install the Service Worker. The install event is the first event a Service Worker receives and is typically used to cache essential resources:
Service Worker Installation
// List of assets to cache during installation
const CACHE_NAME = 'site-static-v1';
const ASSETS = [
'/',
'/index.html',
'/css/styles.css',
'/js/app.js',
'/images/logo.png',
'/offline.html'
];
// Installation event
self.addEventListener('install', event => {
console.log('Service worker installing...');
// Extend the installation process until caching is complete
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('Caching app shell...');
// Add all resources to the cache
return cache.addAll(ASSETS);
})
.then(() => {
// Force activation without waiting
// This is optional and sometimes not recommended for production
console.log('Service worker installed');
return self.skipWaiting();
})
);
});
The skipWaiting() method forces the waiting Service Worker to become the active Service Worker. Without this, the new Service Worker would remain in a "waiting" state until all tabs using the old Service Worker are closed.
Activation
After installation, the Service Worker activates, which is a good time to clean up old caches:
Service Worker Activation
// Activation event
self.addEventListener('activate', event => {
console.log('Service worker activating...');
// Delete old caches
event.waitUntil(
caches.keys()
.then(cacheNames => {
return Promise.all(
cacheNames
.filter(cacheName => {
// Return true if you want to remove this cache
return cacheName.startsWith('site-static-') &&
cacheName !== CACHE_NAME;
})
.map(cacheName => {
console.log('Deleting old cache:', cacheName);
return caches.delete(cacheName);
})
);
})
.then(() => {
// Ensure the SW takes control immediately
console.log('Service worker activated');
return self.clients.claim();
})
);
});
The clients.claim() method makes the newly activated Service Worker take control of uncontrolled pages immediately, rather than waiting for the next navigation.
Idle and Termination
After activation, the Service Worker enters an idle state, ready to respond to events. The browser may terminate an idle Service Worker to save memory, and will restart it when needed.
Update Process
Browsers check for Service Worker updates in certain situations:
- When a user navigates to a page within scope
- When push or sync events occur (at most once per 24 hours)
- When the Service Worker is manually updated via
registration.update()
An update is triggered if the Service Worker file is byte-different from the currently installed version. The update process follows the same lifecycle: the new version installs in the background and waits to activate.
Manual Service Worker Update Check
// Function to check for SW updates
function checkForUpdates() {
if (navigator.serviceWorker) {
navigator.serviceWorker.ready
.then(registration => {
// Check for updates
registration.update();
console.log('Checking for Service Worker updates...');
});
}
}
// Check for updates periodically
setInterval(checkForUpdates, 1000 * 60 * 60); // Once per hour
The Fetch Event
The fetch event is the heart of the Service Worker's ability to control network requests. It fires whenever the browser makes a request within the Service Worker's scope.
Basic Fetch Event Handler
// Fetch event
self.addEventListener('fetch', event => {
console.log('Fetch event for:', event.request.url);
// Respond with a custom strategy
event.respondWith(
// Strategy implementation here
caches.match(event.request)
.then(cachedResponse => {
// Return cached response if available
if (cachedResponse) {
return cachedResponse;
}
// Otherwise, fetch from network
return fetch(event.request);
})
);
});
The event.respondWith() method tells the browser to wait for the provided Promise to resolve with a Response. This gives you complete control over how to respond to each request.
Request and Response Objects
The fetch event provides a Request object that contains information about the request being made. When you respond, you need to provide a Response object:
Working with Request and Response
self.addEventListener('fetch', event => {
// Get details from the request
const url = new URL(event.request.url);
const method = event.request.method;
const headers = event.request.headers;
// Example: Handle specific paths differently
if (url.pathname.startsWith('/api/')) {
// API requests should always go to network
event.respondWith(
fetch(event.request)
.catch(error => {
console.error('API fetch failed:', error);
// Create a custom response
return new Response(
JSON.stringify({ error: 'Network error. Please try again later.' }),
{
status: 503,
statusText: 'Service Unavailable',
headers: {
'Content-Type': 'application/json'
}
}
);
})
);
return;
}
// Handle image requests
if (event.request.destination === 'image') {
event.respondWith(
caches.match(event.request)
.then(cachedResponse => {
if (cachedResponse) {
return cachedResponse;
}
return fetch(event.request)
.then(networkResponse => {
// Clone the response before using it
const responseClone = networkResponse.clone();
// Cache for future
caches.open('images-cache')
.then(cache => {
cache.put(event.request, responseClone);
});
return networkResponse;
})
.catch(() => {
// If fetch fails, return a fallback image
return caches.match('/images/fallback.png');
});
})
);
return;
}
// Default strategy for other requests
// ...
});
Response Cloning
An important concept to understand is that Response bodies can only be consumed once. If you need to use a Response more than once (e.g., to both cache it and return it), you need to clone it first:
Response Cloning Example
return fetch(event.request)
.then(response => {
// Clone the response before using it
const responseToCache = response.clone();
// Cache the response
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, responseToCache);
});
// Return the original response
return response;
});
Implementing Caching Strategies
Caching strategies define how your Service Worker handles requests. Different strategies are appropriate for different types of resources and use cases.
Cache-First Strategy
This strategy checks the cache first, falling back to the network if the resource isn't cached. It's ideal for static assets that change infrequently.
Cache-First Implementation
function cacheFirst(request) {
return caches.match(request)
.then(cachedResponse => {
// Return cached response if available
if (cachedResponse) {
return cachedResponse;
}
// Otherwise fetch from network
return fetch(request)
.then(networkResponse => {
// Clone the response
const responseToCache = networkResponse.clone();
// Add to cache for next time
caches.open(CACHE_NAME)
.then(cache => {
cache.put(request, responseToCache);
});
return networkResponse;
});
});
}
// Use in fetch event
self.addEventListener('fetch', event => {
if (event.request.destination === 'style' ||
event.request.destination === 'script' ||
event.request.destination === 'font') {
event.respondWith(cacheFirst(event.request));
}
});
Network-First Strategy
This strategy tries the network first, falling back to cache if the network request fails. It's good for resources that should be as up-to-date as possible, but where having a slightly outdated version is better than having nothing.
Network-First Implementation
function networkFirst(request) {
return fetch(request)
.then(networkResponse => {
// Clone the response
const responseToCache = networkResponse.clone();
// Add to cache for offline use
caches.open(CACHE_NAME)
.then(cache => {
cache.put(request, responseToCache);
});
return networkResponse;
})
.catch(() => {
// If network fails, try cache
return caches.match(request);
});
}
// Use in fetch event
self.addEventListener('fetch', event => {
if (event.request.destination === 'document' ||
event.request.url.includes('/api/data')) {
event.respondWith(networkFirst(event.request));
}
});
Stale-While-Revalidate Strategy
This strategy returns cached content immediately while fetching an updated version in the background for next time. It provides a good balance between immediacy and freshness.
Stale-While-Revalidate Implementation
function staleWhileRevalidate(request) {
return caches.open(CACHE_NAME)
.then(cache => {
return cache.match(request)
.then(cachedResponse => {
// Create a promise for the network response
const fetchPromise = fetch(request)
.then(networkResponse => {
// Update the cache with the fresh response
cache.put(request, networkResponse.clone());
return networkResponse;
});
// Return the cached response immediately if exists,
// or wait for the network response
return cachedResponse || fetchPromise;
});
});
}
// Use in fetch event
self.addEventListener('fetch', event => {
if (event.request.url.includes('/articles/') ||
event.request.url.includes('/products/')) {
event.respondWith(staleWhileRevalidate(event.request));
}
});
Cache-Only Strategy
This strategy only uses cached resources and never goes to the network. It's useful for static assets that are guaranteed to be in the cache (like the app shell).
Cache-Only Implementation
function cacheOnly(request) {
return caches.match(request);
}
// Use in fetch event
self.addEventListener('fetch', event => {
if (event.request.url.includes('/app-shell/')) {
event.respondWith(cacheOnly(event.request));
}
});
Network-Only Strategy
This strategy always fetches from the network and never uses cache. It's appropriate for non-cacheable or highly dynamic content like API requests with authentication.
Network-Only Implementation
function networkOnly(request) {
return fetch(request);
}
// Use in fetch event
self.addEventListener('fetch', event => {
if (event.request.url.includes('/api/auth/') ||
event.request.url.includes('/payment/')) {
event.respondWith(networkOnly(event.request));
}
});
Combined Strategies
In real applications, you'll likely use different strategies for different types of requests:
Combined Strategies Example
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// App shell resources - Cache First
if (isAppShellResource(event.request)) {
event.respondWith(cacheFirst(event.request));
return;
}
// API calls - Network First with timeout
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirstWithTimeout(event.request, 3000));
return;
}
// Images - Stale While Revalidate
if (event.request.destination === 'image') {
event.respondWith(staleWhileRevalidate(event.request));
return;
}
// HTML pages - Network First
if (event.request.mode === 'navigate') {
event.respondWith(networkFirst(event.request));
return;
}
// Default: Cache First strategy
event.respondWith(cacheFirst(event.request));
});
// Network First with timeout function
function networkFirstWithTimeout(request, timeout) {
return new Promise(resolve => {
// Start a timer for the timeout
const timeoutId = setTimeout(() => {
// If the timer completes, resolve with the cached response
caches.match(request)
.then(cachedResponse => {
if (cachedResponse) {
resolve(cachedResponse);
} else {
// If nothing in cache either, show offline page
resolve(caches.match('/offline.html'));
}
});
}, timeout);
// Try the network
fetch(request)
.then(networkResponse => {
// Clear the timeout timer
clearTimeout(timeoutId);
// Clone the response
const responseToCache = networkResponse.clone();
// Cache the response
caches.open(CACHE_NAME)
.then(cache => {
cache.put(request, responseToCache);
});
// Resolve with the network response
resolve(networkResponse);
})
.catch(error => {
console.error('Fetch failed:', error);
clearTimeout(timeoutId);
// Try the cache
caches.match(request)
.then(cachedResponse => {
if (cachedResponse) {
resolve(cachedResponse);
} else {
// Nothing in cache either, show offline page
resolve(caches.match('/offline.html'));
}
});
});
});
}
graph TD
A[Incoming Request] --> B{Request Type?}
B -->|App Shell| C[Cache First]
B -->|API| D[Network First with Timeout]
B -->|Images| E[Stale While Revalidate]
B -->|HTML Pages| F[Network First]
B -->|Other| G[Cache First]
Creating Effective Offline Fallbacks
A key part of delivering a great offline experience is providing appropriate fallbacks when resources can't be retrieved from either the cache or the network.
Offline Page
A basic offline fallback is to show a custom offline page when the user tries to navigate while offline:
Offline Page Fallback
self.addEventListener('fetch', event => {
// Only handle navigation requests
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request)
.catch(() => {
// If network fetch fails, return the offline page
return caches.match('/offline.html');
})
);
}
});
Generic Fallbacks by Type
You can provide different fallbacks for different types of resources:
Type-Specific Fallbacks
self.addEventListener('fetch', event => {
// Define fallback resources by type
const fallbacks = {
image: '/images/offline-image.png',
font: '/fonts/offline-font.woff2',
document: '/offline.html',
json: JSON.stringify({ error: 'You are offline' })
};
event.respondWith(
// Try the cache
caches.match(event.request)
.then(cachedResponse => {
if (cachedResponse) {
return cachedResponse;
}
// If not in cache, try the network
return fetch(event.request)
.catch(() => {
// If both cache and network fail, use a fallback
const destination = event.request.destination;
// Check if we have a fallback for this type
if (fallbacks[destination]) {
if (destination === 'json') {
// For API requests, return a JSON response
return new Response(
fallbacks[destination],
{
headers: { 'Content-Type': 'application/json' },
status: 503
}
);
} else {
// For other types, get from cache
return caches.match(fallbacks[destination]);
}
}
// No specific fallback, return a generic message
return new Response(
'Sorry, this content is not available offline.',
{
headers: { 'Content-Type': 'text/plain' },
status: 503
}
);
});
})
);
});
Advanced: Data Template Fallbacks
For API responses, you can provide richer fallbacks by combining cached templates with data:
Data Template Fallbacks
self.addEventListener('fetch', event => {
// Handle API requests for product data
if (event.request.url.match(/\/api\/products\/\d+$/)) {
event.respondWith(
// Try the network first
fetch(event.request)
.catch(() => {
// Extract the product ID from the URL
const productId = event.request.url.split('/').pop();
// Try to find cached product data
return caches.match(`/api/products/${productId}`)
.then(cachedProduct => {
if (cachedProduct) {
return cachedProduct;
}
// If no cached product data, return a fallback template
return caches.match('/templates/offline-product.json')
.then(template => {
if (template) {
return template.text()
.then(text => {
// Replace template placeholders with basic info
let fallbackData = text.replace('{{id}}', productId)
.replace('{{name}}', 'Product information unavailable')
.replace('{{status}}', 'offline');
return new Response(
fallbackData,
{
headers: { 'Content-Type': 'application/json' },
status: 200
}
);
});
}
// If no template, return a basic error
return new Response(
JSON.stringify({
error: 'Product information unavailable while offline'
}),
{
headers: { 'Content-Type': 'application/json' },
status: 503
}
);
});
});
})
);
}
});
Designing a Good Offline UX
When designing offline fallbacks, consider these best practices:
- Be Clear: Clearly communicate the offline state to the user
- Provide Options: Offer useful actions the user can take while offline
- Maintain Branding: Keep your offline pages consistent with your site's branding
- Enable Functionality: Allow as much offline functionality as possible
- Queue Actions: For actions that require connectivity, offer to queue them for later
HTML for a Well-Designed Offline Page
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>You're Offline - MyApp</title>
<link rel="stylesheet" href="/css/offline.css">
</head>
<body>
<div class="offline-container">
<header>
<img src="/images/logo-offline.svg" alt="MyApp Logo">
</header>
<main>
<div class="offline-icon">
<svg>...</svg>
</div>
<h1>You're offline</h1>
<p>It looks like you lost your internet connection. Some features may be unavailable until you're back online.</p>
<div class="offline-actions">
<button id="retry-button" class="primary-button">Try Again</button>
<a href="/offline-features" class="secondary-button">See What Works Offline</a>
</div>
<div class="offline-features">
<h2>While you're offline, you can still:</h2>
<ul>
<li>View previously visited pages</li>
<li>Access your saved items</li>
<li>Create new content (will sync when you're back online)</li>
</ul>
</div>
</main>
<footer>
<div class="connection-status">
<span id="status-indicator" class="offline"></span>
<span id="status-text">Offline</span>
</div>
</footer>
</div>
<script>
// Check network status periodically
function checkConnection() {
const statusIndicator = document.getElementById('status-indicator');
const statusText = document.getElementById('status-text');
if (navigator.onLine) {
statusIndicator.className = 'online';
statusText.textContent = 'Online';
} else {
statusIndicator.className = 'offline';
statusText.textContent = 'Offline';
}
}
// Listen for online/offline events
window.addEventListener('online', checkConnection);
window.addEventListener('offline', checkConnection);
// Check initial status
checkConnection();
// Retry button functionality
document.getElementById('retry-button').addEventListener('click', () => {
if (navigator.onLine) {
window.location.reload();
} else {
alert('Still offline. Please check your connection and try again.');
}
});
</script>
</body>
</html>
Communication Between Service Worker and Page
Service Workers and pages can communicate with each other using the postMessage API, enabling coordinated actions and updates.
Sending Messages from Page to Service Worker
Sending Messages to Service Worker
// In your main application
function sendMessageToSW(message) {
// Get the active service worker
navigator.serviceWorker.ready
.then(registration => {
// Send message to the active service worker
registration.active.postMessage(message);
});
}
// Example: Send data to be cached
sendMessageToSW({
action: 'cache-data',
data: {
key: 'user-preferences',
value: { theme: 'dark', fontSize: 'medium' }
}
});
// Example: Trigger a sync
sendMessageToSW({
action: 'sync-data'
});
Receiving Messages in Service Worker
Handling Messages in Service Worker
// In service-worker.js
self.addEventListener('message', event => {
console.log('Message received in SW:', event.data);
// Handle different message types
switch (event.data.action) {
case 'cache-data':
// Cache the provided data
caches.open('app-data')
.then(cache => {
const response = new Response(JSON.stringify(event.data.data.value));
cache.put(`/data/${event.data.data.key}`, response);
console.log(`Data cached: ${event.data.data.key}`);
});
break;
case 'sync-data':
// Trigger background sync
self.registration.sync.register('sync-user-data')
.then(() => {
console.log('Sync registered');
})
.catch(err => {
console.error('Sync registration failed:', err);
});
break;
case 'clear-cache':
// Clear specific cache
caches.delete(event.data.cacheName)
.then(success => {
console.log(`Cache ${event.data.cacheName} deleted:`, success);
});
break;
default:
console.log('Unknown message action:', event.data.action);
}
});
Sending Messages from Service Worker to Page
Sending Messages from Service Worker
// In service-worker.js
function sendMessageToAllClients(message) {
self.clients.matchAll()
.then(clients => {
clients.forEach(client => {
client.postMessage(message);
});
});
}
// Example: Notify about an update
self.addEventListener('install', event => {
// ... normal install code
// Notify clients about the update
sendMessageToAllClients({
action: 'sw-update',
message: 'A new version is available'
});
});
// Example: Notify about completed sync
self.addEventListener('sync', event => {
if (event.tag === 'sync-user-data') {
event.waitUntil(
// ... perform the sync
// Then notify clients when done
syncUserData()
.then(() => {
sendMessageToAllClients({
action: 'sync-complete',
message: 'Your data has been synced'
});
})
);
}
});
Receiving Messages in Page
Handling Messages in Page
// In your main application
navigator.serviceWorker.addEventListener('message', event => {
console.log('Message received from SW:', event.data);
// Handle different message types
switch (event.data.action) {
case 'sw-update':
// Show update notification
showUpdateNotification(event.data.message);
break;
case 'sync-complete':
// Show sync notification
showToast(event.data.message);
break;
case 'cache-status':
// Update UI with cache status
updateCacheStatus(event.data.cacheSize, event.data.lastUpdated);
break;
default:
console.log('Unknown message action:', event.data.action);
}
});
// Example functions for handling messages
function showUpdateNotification(message) {
const notification = document.createElement('div');
notification.className = 'update-notification';
notification.innerHTML = `
${message}
`;
document.body.appendChild(notification);
document.getElementById('update-button').addEventListener('click', () => {
window.location.reload();
});
document.getElementById('dismiss-button').addEventListener('click', () => {
notification.remove();
});
}
function showToast(message) {
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = message;
document.body.appendChild(toast);
// Remove after 3 seconds
setTimeout(() => {
toast.classList.add('fade-out');
setTimeout(() => toast.remove(), 300);
}, 3000);
}
sequenceDiagram
participant Page
participant SW as Service Worker
Note over Page,SW: Bidirectional Communication
Page->>SW: postMessage({ action: "cache-data", ... })
SW->>SW: Process message
SW->>SW: Cache data
SW->>Page: postMessage({ action: "cache-status", ... })
Page->>Page: Update UI
Note over Page,SW: New version available
SW->>Page: postMessage({ action: "sw-update", ... })
Page->>Page: Show update notification
Page->>Page: User clicks "Update"
Page->>Page: location.reload()
Debugging Service Workers
Chrome DevTools
Chrome DevTools provides several features for debugging Service Workers:
- Application Panel: The primary place for Service Worker inspection
- Service Workers Tab: View registered Service Workers, their status, and logs
- Offline Mode: Simulate being offline to test offline functionality
- Cache Storage: Inspect and modify cached resources
graph LR
A[Chrome DevTools] --> B[Application Panel]
B --> C[Service Workers Tab]
B --> D[Cache Storage]
B --> E[Clear Storage]
B --> F[Background Services]
Common Debugging Techniques
Service Worker Debugging Helpers
// Add debug logging to your Service Worker
const DEBUG = true;
function log(...args) {
if (DEBUG) {
console.log('[ServiceWorker]', ...args);
}
}
self.addEventListener('install', event => {
log('Installing Service Worker...');
// ...
});
self.addEventListener('activate', event => {
log('Activating Service Worker...');
// ...
});
self.addEventListener('fetch', event => {
// Log only specific request types to avoid console flood
if (event.request.url.includes('/api/') ||
event.request.mode === 'navigate') {
log('Fetch:', event.request.url, event.request.mode);
}
// ...
});
// Force update of Service Worker during development
self.addEventListener('install', event => {
log('Installing Service Worker...');
// Skip waiting in development
if (DEBUG) {
log('Skipping waiting phase (development mode)');
event.waitUntil(self.skipWaiting());
} else {
// Normal install logic for production
// ...
}
});
// Helper to unregister all Service Workers
// Use in the console when things go wrong
function unregisterAllServiceWorkers() {
navigator.serviceWorker.getRegistrations()
.then(registrations => {
for (let registration of registrations) {
registration.unregister();
console.log('Service Worker unregistered');
}
});
}
// Helper to clear all caches
function clearAllCaches() {
caches.keys()
.then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
console.log('Deleting cache:', cacheName);
return caches.delete(cacheName);
})
);
});
}
Development Strategies
When developing with Service Workers, consider these strategies:
- Use DevTools Reload on Unregister: Forces Service Worker unregistration and page reload
- Cache Versioning: Use version numbers in cache names so new ones are created on changes
- Skip Cache for Development: Add cache-busting URL parameters in development
- Temporary Disable Service Workers: Use the "Bypass for network" option in DevTools
Development Mode Service Worker
// Detect development mode
const isDevelopment = window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1';
// Service Worker registration with development mode handling
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
if (isDevelopment) {
console.log('Development mode: Service Worker handling adjusted');
// Check for query parameter that skips cache
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('no-cache')) {
// Unregister all service workers
navigator.serviceWorker.getRegistrations().then(registrations => {
for (let registration of registrations) {
registration.unregister();
}
console.log('Service Workers unregistered for no-cache mode');
});
// Skip the rest of the registration
return;
}
// In development, register with unique SW file based on timestamp
// This forces the browser to check for changes
const swUrl = `/service-worker.js?dev=${Date.now()}`;
registerServiceWorker(swUrl);
} else {
// Production mode - normal registration
registerServiceWorker('/service-worker.js');
}
});
}
function registerServiceWorker(swUrl) {
navigator.serviceWorker.register(swUrl)
.then(reg => {
console.log('ServiceWorker registered with scope:', reg.scope);
// Check for updates
reg.onupdatefound = () => {
const installingWorker = reg.installing;
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
console.log('New content is available; please refresh.');
// Show update notification in development
if (isDevelopment) {
showUpdateNotification();
}
} else {
console.log('Content is cached for offline use.');
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
Practice Activities
Activity 1: Basic Service Worker Setup
Create a simple web page and implement a basic Service Worker that:
- Registers properly and logs its lifecycle events
- Caches essential resources during installation
- Handles fetch events with a cache-first strategy
- Provides a simple offline fallback page
Test your implementation by taking the site offline (using Chrome DevTools) and verifying it still works.
Activity 2: Multiple Caching Strategies
Extend your Service Worker to implement different caching strategies for different types of resources:
- Static assets (CSS, JS, images): Cache-first strategy
- API responses: Network-first with a 3-second timeout
- HTML pages: Stale-while-revalidate strategy
Test your implementation under various network conditions (fast, slow, offline) to observe the behavior of each strategy.
Activity 3: Service Worker Update Handling
Implement a proper Service Worker update flow that:
- Detects when a new Service Worker version is available
- Notifies the user about the update through a UI element
- Provides options to refresh immediately or later
- Manages cache versions during updates
Test your implementation by modifying the Service Worker file and observing the update flow.
Activity 4: Advanced Offline Experience
Create an enhanced offline experience that:
- Shows a well-designed offline page with your site's branding
- Provides offline fallbacks for images and other assets
- Implements a "retry" button that checks for connection
- Indicates the connection status (online/offline) in the UI
- Stores user input while offline for later submission
Test your implementation by turning off your network and navigating through the site.
Summary
Service Workers are a powerful technology that enables Progressive Web Apps to work reliably, even in challenging network conditions. In this lecture, we've covered:
- Service Worker Basics: Understanding what Service Workers are and how they function as independent JavaScript threads
- Lifecycle Management: The registration, installation, activation, and update process of Service Workers
- Fetch Event Handling: Intercepting and responding to network requests
- Caching Strategies: Implementing various strategies like cache-first, network-first, and stale-while-revalidate
- Offline Fallbacks: Creating effective fallback experiences when the network is unavailable
- Communication: Messaging between Service Workers and web pages
- Debugging Techniques: Tools and approaches for troubleshooting Service Worker issues
By mastering these concepts and techniques, you can create web applications that:
- Load instantly, even on repeat visits
- Work regardless of network conditions
- Feel responsive and reliable to users
- Provide meaningful experiences, even when offline
Service Workers represent a fundamental shift in how we think about web applications, moving from the traditional assumption of constant connectivity to a more resilient model that embraces the reality of variable network conditions.