Index · How it works
1 min readRapid overview
- How it works
- Table of Contents
- Service Worker Fundamentals
- What is a Service Worker?
- Basic Service Worker Registration
- Service Worker Lifecycle
- PWA Requirements
- Web App Manifest
- HTML Requirements
- Caching Strategies
- 1. Cache First (Offline First)
- 2. Network First (Network Falling Back to Cache)
- 3. Stale While Revalidate
- 4. Network Only
- 5. Cache Only
- Advanced: Strategy by Request Type
- Background Sync
- Register Background Sync
- Handle Sync Event in Service Worker
- Push Notifications
- Request Permission
- Handle Push Events
- Installation & Updates
- Prompt User to Install PWA
- Handle Service Worker Updates
- Best Practices
- 1. Versioning and Cache Management
- 2. Handle Offline Gracefully
- 3. Scope and HTTPS Requirements
- 4. Testing Service Workers
How it works
Table of Contents
- Service Worker Fundamentals
- PWA Requirements
- Caching Strategies
- Background Sync
- Push Notifications
- Installation & Updates
- Best Practices
Service Worker Fundamentals
What is a Service Worker?
A Service Worker is a JavaScript file that runs in the background, separate from the web page, enabling features that don't need a web page or user interaction.
Basic Service Worker Registration
// main.js - Register service worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/'
});
console.log('Service Worker registered:', registration.scope);
// Listen for updates
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
console.log('New service worker installing...');
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'activated') {
console.log('New service worker activated!');
}
});
});
} catch (error) {
console.error('Service Worker registration failed:', error);
}
});
}
Service Worker Lifecycle
// sw.js - Service Worker file
const CACHE_VERSION = 'v1';
const CACHE_NAME = `app-cache-${CACHE_VERSION}`;
// 1. Install Event - Cache static assets
self.addEventListener('install', (event) => {
console.log('Service Worker installing...');
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll([
'/',
'/index.html',
'/styles.css',
'/app.js',
'/manifest.json',
'/images/icon-192.png',
'/images/icon-512.png'
]);
}).then(() => {
// Skip waiting to activate immediately
return self.skipWaiting();
})
);
});
// 2. Activate Event - Clean up old caches
self.addEventListener('activate', (event) => {
console.log('Service Worker activating...');
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name))
);
}).then(() => {
// Take control of all pages immediately
return self.clients.claim();
})
);
});
// 3. Fetch Event - Intercept network requests
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}
return fetch(event.request);
})
);
});
PWA Requirements
Web App Manifest
// manifest.json
{
"name": "My Progressive Web App",
"short_name": "My PWA",
"description": "A progressive web application",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#3f51b5",
"orientation": "portrait-primary",
"icons": [
{
"src": "/images/icon-72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "/images/icon-96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/images/icon-128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "/images/icon-144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "/images/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/images/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"screenshots": [
{
"src": "/images/screenshot1.png",
"sizes": "540x720",
"type": "image/png"
}
],
"categories": ["productivity", "utilities"],
"share_target": {
"action": "/share",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"title": "title",
"text": "text",
"url": "url"
}
}
}
HTML Requirements
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- PWA Manifest -->
<link rel="manifest" href="/manifest.json">
<!-- Theme Color -->
<meta name="theme-color" content="#3f51b5">
<!-- Apple Touch Icons -->
<link rel="apple-touch-icon" href="/images/icon-192.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<!-- MS Tiles -->
<meta name="msapplication-TileImage" content="/images/icon-144.png">
<meta name="msapplication-TileColor" content="#3f51b5">
<title>My PWA</title>
</head>
<body>
<div id="app"></div>
<script src="/app.js"></script>
</body>
</html>
Caching Strategies
1. Cache First (Offline First)
Best for static assets that rarely change.
// Cache first, fall back to network
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
return cachedResponse || fetch(event.request).then((response) => {
// Cache the new response for future use
return caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, response.clone());
return response;
});
});
}).catch(() => {
// Return offline page if both cache and network fail
return caches.match('/offline.html');
})
);
});
2. Network First (Network Falling Back to Cache)
Best for frequently updated content.
// Network first, fall back to cache
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.then((response) => {
// Update cache with fresh response
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseClone);
});
return response;
})
.catch(() => {
// Network failed, try cache
return caches.match(event.request);
})
);
});
3. Stale While Revalidate
Best for content that can be slightly outdated.
// Return cached version immediately, update cache in background
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.open(CACHE_NAME).then((cache) => {
return cache.match(event.request).then((cachedResponse) => {
const fetchPromise = fetch(event.request).then((networkResponse) => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
// Return cached response immediately, or wait for network
return cachedResponse || fetchPromise;
});
})
);
});
4. Network Only
For requests that must always be fresh (POST, real-time data).
self.addEventListener('fetch', (event) => {
if (event.request.method === 'POST') {
event.respondWith(fetch(event.request));
}
});
5. Cache Only
For pre-cached static assets.
self.addEventListener('fetch', (event) => {
event.respondWith(caches.match(event.request));
});
Advanced: Strategy by Request Type
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// API requests - Network first
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(request));
}
// Images - Cache first
else if (request.destination === 'image') {
event.respondWith(cacheFirst(request));
}
// HTML pages - Stale while revalidate
else if (request.destination === 'document') {
event.respondWith(staleWhileRevalidate(request));
}
// Everything else - Network first
else {
event.respondWith(networkFirst(request));
}
});
async function cacheFirst(request) {
const cache = await caches.open(CACHE_NAME);
const cached = await cache.match(request);
if (cached) return cached;
const response = await fetch(request);
cache.put(request, response.clone());
return response;
}
async function networkFirst(request) {
try {
const response = await fetch(request);
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
return response;
} catch (error) {
const cached = await caches.match(request);
if (cached) return cached;
throw error;
}
}
async function staleWhileRevalidate(request) {
const cache = await caches.open(CACHE_NAME);
const cached = await cache.match(request);
const fetchPromise = fetch(request).then((response) => {
cache.put(request, response.clone());
return response;
});
return cached || fetchPromise;
}
Background Sync
Register Background Sync
// Register sync when online status changes
async function registerBackgroundSync() {
if ('serviceWorker' in navigator && 'sync' in self.registration) {
try {
await navigator.serviceWorker.ready;
await self.registration.sync.register('sync-messages');
console.log('Background sync registered');
} catch (error) {
console.error('Background sync failed:', error);
}
}
}
// Queue data for sync when offline
async function sendMessage(message) {
if (navigator.onLine) {
// Send immediately if online
await fetch('/api/messages', {
method: 'POST',
body: JSON.stringify(message)
});
} else {
// Queue for background sync
const db = await openDB();
await db.add('pending-messages', message);
await registerBackgroundSync();
}
}
Handle Sync Event in Service Worker
// sw.js
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-messages') {
event.waitUntil(syncMessages());
}
});
async function syncMessages() {
const db = await openDB();
const messages = await db.getAll('pending-messages');
for (const message of messages) {
try {
await fetch('/api/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(message)
});
await db.delete('pending-messages', message.id);
} catch (error) {
console.error('Failed to sync message:', error);
// Will retry on next sync
}
}
}
Push Notifications
Request Permission
// Request notification permission
async function requestNotificationPermission() {
if (!('Notification' in window)) {
console.log('Notifications not supported');
return false;
}
const permission = await Notification.requestPermission();
return permission === 'granted';
}
// Subscribe to push notifications
async function subscribeToPush() {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY)
});
// Send subscription to server
await fetch('/api/push/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription)
});
return subscription;
}
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
const rawData = window.atob(base64);
return Uint8Array.from([...rawData].map(char => char.charCodeAt(0)));
}
Handle Push Events
// sw.js - Handle push notifications
self.addEventListener('push', (event) => {
const data = event.data?.json() ?? {};
const title = data.title || 'New Notification';
const options = {
body: data.body || 'You have a new message',
icon: '/images/icon-192.png',
badge: '/images/badge-72.png',
tag: data.tag || 'default',
data: data.url,
actions: [
{ action: 'open', title: 'Open' },
{ action: 'close', title: 'Close' }
],
vibrate: [200, 100, 200],
requireInteraction: false
};
event.waitUntil(
self.registration.showNotification(title, options)
);
});
// Handle notification clicks
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action === 'open' || !event.action) {
const urlToOpen = event.notification.data || '/';
event.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true })
.then((clientList) => {
// Focus existing window if available
for (const client of clientList) {
if (client.url === urlToOpen && 'focus' in client) {
return client.focus();
}
}
// Open new window
if (clients.openWindow) {
return clients.openWindow(urlToOpen);
}
})
);
}
});
Installation & Updates
Prompt User to Install PWA
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (event) => {
// Prevent default prompt
event.preventDefault();
deferredPrompt = event;
// Show custom install button
showInstallButton();
});
async function showInstallPrompt() {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log(`User ${outcome === 'accepted' ? 'accepted' : 'dismissed'} install`);
deferredPrompt = null;
}
// Detect if app was installed
window.addEventListener('appinstalled', () => {
console.log('PWA was installed');
hideInstallButton();
});
// Detect if running as PWA
function isPWA() {
return window.matchMedia('(display-mode: standalone)').matches ||
window.navigator.standalone === true;
}
Handle Service Worker Updates
async function checkForUpdates() {
const registration = await navigator.serviceWorker.ready;
await registration.update();
}
navigator.serviceWorker.addEventListener('controllerchange', () => {
// New service worker has taken control
console.log('New service worker activated');
window.location.reload();
});
// Show update notification
function showUpdateNotification(registration) {
const notification = document.createElement('div');
notification.innerHTML = `
<div class="update-banner">
<p>A new version is available!</p>
<button id="reload-btn">Reload</button>
</div>
`;
document.body.appendChild(notification);
document.getElementById('reload-btn').addEventListener('click', () => {
const waiting = registration.waiting;
if (waiting) {
waiting.postMessage({ type: 'SKIP_WAITING' });
}
});
}
// sw.js - Handle skip waiting message
self.addEventListener('message', (event) => {
if (event.data?.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
Best Practices
1. Versioning and Cache Management
const VERSION = '1.2.3';
const CACHE_NAME = `app-v${VERSION}`;
const CACHE_ASSETS = [
'/',
'/offline.html',
'/styles.css',
'/app.js'
];
// Use versioned cache names
async function updateCache() {
const cache = await caches.open(CACHE_NAME);
await cache.addAll(CACHE_ASSETS);
}
// Clean up old versions
async function cleanupCaches() {
const keys = await caches.keys();
await Promise.all(
keys
.filter(key => key !== CACHE_NAME)
.map(key => caches.delete(key))
);
}
2. Handle Offline Gracefully
// Detect online/offline status
window.addEventListener('online', () => {
console.log('Back online');
document.body.classList.remove('offline');
syncPendingRequests();
});
window.addEventListener('offline', () => {
console.log('Gone offline');
document.body.classList.add('offline');
showOfflineBanner();
});
// Initial check
if (!navigator.onLine) {
document.body.classList.add('offline');
}
3. Scope and HTTPS Requirements
- Service Workers only work over HTTPS (except localhost)
- Set appropriate scope for service worker
- Use secure headers
// Register with specific scope
navigator.serviceWorker.register('/sw.js', {
scope: '/app/' // Only control /app/* URLs
});
4. Testing Service Workers
// Unregister all service workers (for testing)
async function unregisterAllServiceWorkers() {
const registrations = await navigator.serviceWorker.getRegistrations();
await Promise.all(registrations.map(r => r.unregister()));
}
// Clear all caches (for testing)
async function clearAllCaches() {
const keys = await caches.keys();
await Promise.all(keys.map(key => caches.delete(key)));
}