Service workers enable offline functionality, background sync, and push notifications.
Registering a Service Worker#
1// main.js
2if ('serviceWorker' in navigator) {
3 window.addEventListener('load', async () => {
4 try {
5 const registration = await navigator.serviceWorker.register('/sw.js');
6 console.log('SW registered:', registration.scope);
7 } catch (error) {
8 console.log('SW registration failed:', error);
9 }
10 });
11}Basic Service Worker#
1// sw.js
2const CACHE_NAME = 'app-v1';
3const ASSETS = [
4 '/',
5 '/index.html',
6 '/styles.css',
7 '/app.js',
8 '/offline.html',
9];
10
11// Install - cache assets
12self.addEventListener('install', (event) => {
13 event.waitUntil(
14 caches.open(CACHE_NAME).then((cache) => {
15 return cache.addAll(ASSETS);
16 })
17 );
18 self.skipWaiting();
19});
20
21// Activate - clean old caches
22self.addEventListener('activate', (event) => {
23 event.waitUntil(
24 caches.keys().then((keys) => {
25 return Promise.all(
26 keys
27 .filter((key) => key !== CACHE_NAME)
28 .map((key) => caches.delete(key))
29 );
30 })
31 );
32 self.clients.claim();
33});Caching Strategies#
Cache First (Static Assets)#
1self.addEventListener('fetch', (event) => {
2 event.respondWith(
3 caches.match(event.request).then((cached) => {
4 return cached || fetch(event.request);
5 })
6 );
7});Network First (API Requests)#
1self.addEventListener('fetch', (event) => {
2 if (event.request.url.includes('/api/')) {
3 event.respondWith(
4 fetch(event.request)
5 .then((response) => {
6 const clone = response.clone();
7 caches.open(CACHE_NAME).then((cache) => {
8 cache.put(event.request, clone);
9 });
10 return response;
11 })
12 .catch(() => caches.match(event.request))
13 );
14 }
15});Stale While Revalidate#
1self.addEventListener('fetch', (event) => {
2 event.respondWith(
3 caches.open(CACHE_NAME).then((cache) => {
4 return cache.match(event.request).then((cached) => {
5 const fetchPromise = fetch(event.request).then((response) => {
6 cache.put(event.request, response.clone());
7 return response;
8 });
9 return cached || fetchPromise;
10 });
11 })
12 );
13});Complete Strategy Router#
1const CACHE_NAME = 'app-v1';
2
3self.addEventListener('fetch', (event) => {
4 const { request } = event;
5 const url = new URL(request.url);
6
7 // Static assets - cache first
8 if (request.destination === 'style' ||
9 request.destination === 'script' ||
10 request.destination === 'image') {
11 event.respondWith(cacheFirst(request));
12 return;
13 }
14
15 // API requests - network first
16 if (url.pathname.startsWith('/api/')) {
17 event.respondWith(networkFirst(request));
18 return;
19 }
20
21 // HTML pages - stale while revalidate
22 if (request.mode === 'navigate') {
23 event.respondWith(staleWhileRevalidate(request));
24 return;
25 }
26});
27
28async function cacheFirst(request) {
29 const cached = await caches.match(request);
30 return cached || fetch(request);
31}
32
33async function networkFirst(request) {
34 try {
35 const response = await fetch(request);
36 const cache = await caches.open(CACHE_NAME);
37 cache.put(request, response.clone());
38 return response;
39 } catch {
40 return caches.match(request);
41 }
42}
43
44async function staleWhileRevalidate(request) {
45 const cache = await caches.open(CACHE_NAME);
46 const cached = await cache.match(request);
47
48 const fetchPromise = fetch(request).then((response) => {
49 cache.put(request, response.clone());
50 return response;
51 });
52
53 return cached || fetchPromise;
54}Background Sync#
1// In your app
2async function saveData(data) {
3 try {
4 await fetch('/api/save', { method: 'POST', body: JSON.stringify(data) });
5 } catch {
6 // Queue for background sync
7 const registration = await navigator.serviceWorker.ready;
8 await registration.sync.register('sync-data');
9
10 // Store data in IndexedDB for later
11 await saveToIndexedDB('pending-sync', data);
12 }
13}
14
15// In service worker
16self.addEventListener('sync', (event) => {
17 if (event.tag === 'sync-data') {
18 event.waitUntil(syncPendingData());
19 }
20});
21
22async function syncPendingData() {
23 const pending = await getFromIndexedDB('pending-sync');
24
25 for (const data of pending) {
26 await fetch('/api/save', { method: 'POST', body: JSON.stringify(data) });
27 await removeFromIndexedDB('pending-sync', data.id);
28 }
29}Push Notifications#
1// Subscribe to push
2async function subscribeToPush() {
3 const registration = await navigator.serviceWorker.ready;
4
5 const subscription = await registration.pushManager.subscribe({
6 userVisibleOnly: true,
7 applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
8 });
9
10 await fetch('/api/push/subscribe', {
11 method: 'POST',
12 body: JSON.stringify(subscription),
13 });
14}
15
16// In service worker
17self.addEventListener('push', (event) => {
18 const data = event.data.json();
19
20 event.waitUntil(
21 self.registration.showNotification(data.title, {
22 body: data.body,
23 icon: '/icon.png',
24 data: { url: data.url },
25 })
26 );
27});
28
29self.addEventListener('notificationclick', (event) => {
30 event.notification.close();
31 event.waitUntil(
32 clients.openWindow(event.notification.data.url)
33 );
34});Service workers unlock powerful offline experiences with proper caching and background sync.