Progressive Web Apps (PWA)

Service Worker Registration and Installation

20 min Lesson 4 of 30

Registering a Service Worker

Before a service worker can control your pages and intercept requests, it must be registered. Registration tells the browser where your service worker file is located and what scope it should control.

Basic Registration

// main.js - Run this in your main JavaScript file if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js') .then(registration => { console.log('Service Worker registered successfully'); console.log('Scope:', registration.scope); }) .catch(error => { console.error('Service Worker registration failed:', error); }); }); }
Why wait for 'load' event?

Registering on window.addEventListener('load') ensures the service worker registration doesn't compete with initial page load resources, improving performance.

Registration with Options

// Register with custom scope navigator.serviceWorker.register('/sw.js', { scope: '/app/' }) .then(registration => { console.log('SW registered with scope:', registration.scope); }); // Register with update checks navigator.serviceWorker.register('/sw.js', { updateViaCache: 'none' // 'imports', 'all', 'none' }) .then(registration => { // Check for updates every hour setInterval(() => { registration.update(); }, 3600000); });
updateViaCache Options:
  • 'imports': (default) Cache service worker but not imported scripts
  • 'all': Cache service worker and all imported scripts
  • 'none': Don't cache service worker or imported scripts (always fetch fresh)

The Installation Phase

After successful registration, the browser downloads and installs the service worker. The install event is the perfect time to cache static assets.

Basic Install Event

// sw.js const CACHE_NAME = 'my-pwa-cache-v1'; const urlsToCache = [ '/', '/index.html', '/styles/main.css', '/scripts/app.js', '/images/logo.png' ]; self.addEventListener('install', event => { console.log('Service Worker: Installing...'); // Wait until caching is complete event.waitUntil( caches.open(CACHE_NAME) .then(cache => { console.log('Service Worker: Caching files'); return cache.addAll(urlsToCache); }) .then(() => { console.log('Service Worker: All files cached'); }) ); });
Understanding event.waitUntil():

The waitUntil() method extends the event lifetime until the promise resolves. This ensures the service worker won't be terminated before caching completes.

  • If promise resolves → installation succeeds
  • If promise rejects → installation fails, service worker is discarded
  • Browser retries installation on next page load

Cache API Methods

1. caches.open()

// Open or create a cache caches.open('my-cache-v1') .then(cache => { // Cache object ready to use });

2. cache.addAll()

// Add multiple URLs to cache (atomic operation) cache.addAll([ '/', '/styles/main.css', '/scripts/app.js' ]) .then(() => { console.log('All files cached'); }) .catch(error => { console.error('Failed to cache files:', error); // If ANY file fails, entire operation fails });
cache.addAll() is Atomic:

If even one URL fails to cache (404, network error), the entire operation fails and the service worker installation is rejected. Use cache.add() in a loop with error handling for non-critical resources.

3. cache.add()

// Add single URL to cache cache.add('/styles/main.css') .then(() => { console.log('File cached'); }); // Equivalent to: fetch('/styles/main.css') .then(response => cache.put('/styles/main.css', response));

4. cache.put()

// Cache a request/response pair manually fetch('/api/data.json') .then(response => { // Clone response because body can only be read once return cache.put('/api/data.json', response.clone()); });

Advanced Install Pattern: Critical vs Optional

// sw.js const CACHE_NAME = 'pwa-v1'; // Critical assets - installation fails if these don't cache const criticalAssets = [ '/', '/index.html', '/styles/main.css', '/scripts/app.js' ]; // Optional assets - installation succeeds even if these fail const optionalAssets = [ '/images/banner.jpg', '/images/background.jpg', '/fonts/custom-font.woff2' ]; self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME) .then(cache => { // Cache critical assets (atomic) return cache.addAll(criticalAssets) .then(() => { // Cache optional assets individually (non-atomic) return Promise.all( optionalAssets.map(url => { return cache.add(url).catch(error => { console.warn(`Failed to cache ${url}:`, error); // Don't reject - allow installation to continue }); }) ); }); }) ); });

Skip Waiting

By default, a new service worker waits in the "waiting" state until all pages using the old service worker close. skipWaiting() forces immediate activation.

// sw.js self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME) .then(cache => cache.addAll(urlsToCache)) .then(() => { // Force new service worker to activate immediately return self.skipWaiting(); }) ); });
skipWaiting() Considerations:
  • Pro: Users get updates immediately without closing tabs
  • Con: Can cause inconsistency if old and new versions conflict
  • Best practice: Use with caution, combine with page reload prompt
  • Alternative: Prompt user to reload when update available

The Activation Phase

After installation (and waiting, if applicable), the service worker activates. The activate event is the ideal time to clean up old caches.

// sw.js const CACHE_NAME = 'pwa-v2'; self.addEventListener('activate', event => { console.log('Service Worker: Activating...'); event.waitUntil( caches.keys() .then(cacheNames => { // Delete all caches except current version return Promise.all( cacheNames.map(cacheName => { if (cacheName !== CACHE_NAME) { console.log('Service Worker: Deleting old cache:', cacheName); return caches.delete(cacheName); } }) ); }) .then(() => { console.log('Service Worker: Cache cleanup complete'); }) ); });

Claiming Clients

By default, a newly activated service worker won't control pages loaded before it activated. clients.claim() takes immediate control.

// sw.js self.addEventListener('activate', event => { event.waitUntil( // Clean up old caches caches.keys() .then(cacheNames => { return Promise.all( cacheNames .filter(cacheName => cacheName !== CACHE_NAME) .map(cacheName => caches.delete(cacheName)) ); }) .then(() => { // Take control of all pages immediately return self.clients.claim(); }) ); });
clients.claim() Use Cases:
  • First-time installation - start controlling immediately
  • Critical updates - apply changes without reload
  • Combine with skipWaiting() for instant updates
  • Best with page reload notification

Service Worker Versioning

// sw.js // Version in cache name const VERSION = 'v3.1.0'; const CACHE_NAME = `pwa-${VERSION}`; // Or use timestamp const TIMESTAMP = new Date().getTime(); const CACHE_NAME = `pwa-${TIMESTAMP}`; // Or use hash (in build process) const HASH = 'abc123def456'; const CACHE_NAME = `pwa-${HASH}`;
Versioning Best Practices:
  • Change cache name to trigger update
  • Use semantic versioning (v1.0.0, v1.1.0, etc.)
  • Or use build timestamps/hashes
  • Keep version at top of file for easy updates
  • Update version with every cache change

Complete Registration and Installation Example

// ========== main.js (Page Script) ========== if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js') .then(registration => { console.log('SW registered:', registration.scope); // Listen for updates registration.addEventListener('updatefound', () => { const newWorker = registration.installing; newWorker.addEventListener('statechange', () => { if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { // New version available if (confirm('New version available! Reload to update?')) { newWorker.postMessage({ action: 'skipWaiting' }); window.location.reload(); } } }); }); }) .catch(error => { console.error('SW registration failed:', error); }); // Listen for controller change (new SW activated) navigator.serviceWorker.addEventListener('controllerchange', () => { console.log('New service worker activated'); window.location.reload(); }); }); } // ========== sw.js (Service Worker) ========== const VERSION = 'v1.0.0'; const CACHE_NAME = `pwa-${VERSION}`; const STATIC_ASSETS = [ '/', '/index.html', '/styles/main.css', '/scripts/app.js', '/manifest.json' ]; // Install event self.addEventListener('install', event => { console.log(`SW ${VERSION}: Installing...`); event.waitUntil( caches.open(CACHE_NAME) .then(cache => cache.addAll(STATIC_ASSETS)) .then(() => self.skipWaiting()) .then(() => console.log(`SW ${VERSION}: Installed`)) ); }); // Activate event self.addEventListener('activate', event => { console.log(`SW ${VERSION}: Activating...`); event.waitUntil( caches.keys() .then(cacheNames => { return Promise.all( cacheNames .filter(name => name !== CACHE_NAME) .map(name => { console.log(`SW ${VERSION}: Deleting cache ${name}`); return caches.delete(name); }) ); }) .then(() => self.clients.claim()) .then(() => console.log(`SW ${VERSION}: Activated`)) ); }); // Message handler for skipWaiting self.addEventListener('message', event => { if (event.data && event.data.action === 'skipWaiting') { self.skipWaiting(); } });

Debugging Installation

Chrome DevTools Debugging:
  1. Open DevTools → Application → Service Workers
  2. Check "Update on reload" to force updates during development
  3. Use "Unregister" to remove service worker completely
  4. Click service worker source link to see install/activate logs
  5. Check "Bypass for network" to disable service worker temporarily
  6. View Cache Storage to verify cached files
Exercise:
  1. Create a new HTML file with a basic page structure
  2. Create a sw.js file and implement install/activate events
  3. Cache at least 3 static assets during installation
  4. Register the service worker in your page's JavaScript
  5. Open Chrome DevTools and verify successful registration
  6. Check Cache Storage to confirm files are cached
  7. Change the cache version, reload, and verify old cache is deleted
  8. Implement skipWaiting() and clients.claim() and observe the difference
  9. Add console.log statements and observe the lifecycle in DevTools