Service Workers and Background APIs

1. Service Worker Registration and Lifecycle

Method Syntax Description Browser Support
register navigator.serviceWorker.register(scriptURL, options) Registers service worker. Returns Promise<ServiceWorkerRegistration>. Requires HTTPS. All Modern Browsers
getRegistration navigator.serviceWorker.getRegistration(scope) Gets existing registration for scope. Returns Promise<ServiceWorkerRegistration>. All Modern Browsers
getRegistrations navigator.serviceWorker.getRegistrations() Gets all registrations. Returns Promise<ServiceWorkerRegistration[]>. All Modern Browsers
Lifecycle Event When Fired Use Case
install Service worker first installed. Runs only once per version. Cache static assets, setup
activate Service worker activated. After install or on page reload for updated worker. Clean old caches, claim clients
fetch Network request intercepted. Fires for all page requests. Cache strategies, offline support
message Message received from client (page/worker). Client-worker communication
sync Background sync triggered (when online). Retry failed requests
push Push notification received. Show notifications
ServiceWorkerRegistration Property Type Description
installing ServiceWorker Service worker currently installing. null if none.
waiting ServiceWorker Service worker installed, waiting to activate. null if none.
active ServiceWorker Active service worker controlling pages. null if none.
scope string URL scope of service worker (e.g., "/app/").
updateViaCache string Cache mode for updates: "imports", "all", "none".

Example: Register service worker

// Check support
if ("serviceWorker" in navigator) {
  console.log("Service Workers supported");
} else {
  console.log("Service Workers not supported");
}

// Register service worker
async function registerServiceWorker() {
  try {
    const registration = await navigator.serviceWorker.register("/sw.js", {
      "scope": "/" // Default is script location
    });
    
    console.log("Service Worker registered:", registration.scope);
    
    // Check state
    if (registration.installing) {
      console.log("Service Worker installing");
    } else if (registration.waiting) {
      console.log("Service Worker waiting");
    } else if (registration.active) {
      console.log("Service Worker active");
    }
    
    return registration;
  } catch (error) {
    console.error("Service Worker registration failed:", error);
  }
}

// Register on page load
window.addEventListener("load", () => {
  registerServiceWorker();
});

// Listen for updates
navigator.serviceWorker.addEventListener("controllerchange", () => {
  console.log("Service Worker controller changed");
  // New service worker took control
  window.location.reload();
});

// Check for updates manually
async function checkForUpdates() {
  const registration = await navigator.serviceWorker.getRegistration();
  if (registration) {
    await registration.update();
    console.log("Checked for updates");
  }
}

// Unregister service worker
async function unregisterServiceWorker() {
  const registration = await navigator.serviceWorker.getRegistration();
  if (registration) {
    const success = await registration.unregister();
    console.log("Unregistered:", success);
  }
}

Example: Service worker lifecycle (sw.js)

const CACHE_NAME = "my-app-v1";
const STATIC_ASSETS = [
  "/",
  "/index.html",
  "/styles.css",
  "/app.js",
  "/logo.png"
];

// Install event - cache static assets
self.addEventListener("install", (event) => {
  console.log("Service Worker installing");
  
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      console.log("Caching static assets");
      return cache.addAll(STATIC_ASSETS);
    }).then(() => {
      // Skip waiting to activate immediately
      return self.skipWaiting();
    })
  );
});

// Activate event - clean 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) => {
            console.log("Deleting old cache:", name);
            return caches.delete(name);
          })
      );
    }).then(() => {
      // Take control of all clients immediately
      return self.clients.claim();
    })
  );
});

// Fetch event - serve from cache or network
self.addEventListener("fetch", (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      // Cache hit - return cached response
      if (response) {
        return response;
      }
      
      // Cache miss - fetch from network
      return fetch(event.request).then((response) => {
        // Don't cache non-GET requests or non-ok responses
        if (event.request.method !== "GET" || !response.ok) {
          return response;
        }
        
        // Clone response (can only read once)
        const responseToCache = response.clone();
        
        // Cache for next time
        caches.open(CACHE_NAME).then((cache) => {
          cache.put(event.request, responseToCache);
        });
        
        return response;
      });
    }).catch(() => {
      // Network failed - return offline page
      return caches.match("/offline.html");
    })
  );
});
Note: Service Workers require HTTPS (localhost OK for development). Only one service worker active per scope. Use skipWaiting() to activate immediately, clients.claim() to control existing pages. Updates check every 24 hours or on navigation. Use versioned cache names to manage updates.
Warning: Service worker has no DOM access - can't manipulate page directly. Runs in separate thread. Use event.waitUntil() to extend event lifetime for async operations. Service worker can be terminated anytime - don't rely on global state. Test offline behavior thoroughly.

2. Service Worker Message Passing and Communication

Method From To Syntax
postMessage Page Service Worker navigator.serviceWorker.controller.postMessage(data)
postMessage Service Worker Page client.postMessage(data)
postMessage Service Worker All Pages clients.matchAll().then(clients => clients.forEach(c => c.postMessage(data)))
Clients Method Description Use Case
clients.matchAll(options) Gets all client windows/tabs. Options: includeUncontrolled, type. Broadcast to all tabs
clients.get(id) Gets specific client by ID. Returns Promise<Client>. Reply to specific tab
clients.openWindow(url) Opens new window/tab. Returns Promise<WindowClient>. Requires user interaction. Open notification click
clients.claim() Makes service worker control all clients immediately (without reload). Take control on activate

Example: Page to service worker communication

// From page to service worker
if (navigator.serviceWorker.controller) {
  // Send message
  navigator.serviceWorker.controller.postMessage({
    "type": "CACHE_URLS",
    "urls": ["/page1.html", "/page2.html"]
  });
  
  console.log("Message sent to service worker");
} else {
  console.log("No active service worker");
}

// Listen for messages from service worker
navigator.serviceWorker.addEventListener("message", (event) => {
  console.log("Message from service worker:", event.data);
  
  if (event.data.type === "CACHE_UPDATED") {
    console.log("Cache updated:", event.data.urls);
  } else if (event.data.type === "NEW_VERSION") {
    showUpdatePrompt();
  }
});

// Request-response pattern with message channel
function sendMessageWithResponse(message) {
  return new Promise((resolve, reject) => {
    const messageChannel = new MessageChannel();
    
    messageChannel.port1.onmessage = (event) => {
      if (event.data.error) {
        reject(event.data.error);
      } else {
        resolve(event.data);
      }
    };
    
    navigator.serviceWorker.controller.postMessage(
      message,
      [messageChannel.port2]
    );
  });
}

// Usage
async function getCacheInfo() {
  try {
    const response = await sendMessageWithResponse({
      "type": "GET_CACHE_INFO"
    });
    console.log("Cache info:", response);
  } catch (error) {
    console.error("Error:", error);
  }
}

Example: Service worker to page communication

// In service worker (sw.js)

// Listen for messages from pages
self.addEventListener("message", (event) => {
  console.log("Message received:", event.data);
  
  if (event.data.type === "CACHE_URLS") {
    // Cache requested URLs
    cacheUrls(event.data.urls).then(() => {
      // Reply to sender
      event.source.postMessage({
        "type": "CACHE_UPDATED",
        "urls": event.data.urls
      });
    });
  } else if (event.data.type === "GET_CACHE_INFO") {
    // Handle request-response with message channel
    caches.keys().then((cacheNames) => {
      event.ports[0].postMessage({
        "caches": cacheNames,
        "count": cacheNames.length
      });
    });
  } else if (event.data.type === "SKIP_WAITING") {
    self.skipWaiting();
  }
});

// Broadcast to all clients
async function notifyAllClients(message) {
  const clients = await self.clients.matchAll({
    "includeUncontrolled": true,
    "type": "window"
  });
  
  clients.forEach((client) => {
    client.postMessage(message);
  });
}

// Notify when cache updated
async function cacheUrls(urls) {
  const cache = await caches.open(CACHE_NAME);
  await cache.addAll(urls);
  
  // Notify all tabs
  await notifyAllClients({
    "type": "CACHE_UPDATED",
    "urls": urls
  });
}

// Notify specific client
async function notifyClient(clientId, message) {
  const client = await self.clients.get(clientId);
  if (client) {
    client.postMessage(message);
  }
}

// Open window on notification click
self.addEventListener("notificationclick", (event) => {
  event.notification.close();
  
  event.waitUntil(
    clients.openWindow("/notifications")
  );
});
Note: postMessage only works if service worker is active and controlling page. Check navigator.serviceWorker.controller before sending. Messages are structured clones - can't send functions, DOM nodes. Use MessageChannel for request-response patterns.

3. Background Sync and Periodic Sync APIs

API Method Description Browser Support
Background Sync registration.sync.register(tag) Registers one-time sync when online. Returns Promise. Chrome, Edge
Periodic Sync registration.periodicSync.register(tag, options) Registers periodic sync. Options: minInterval in ms. Requires installed PWA. Limited (Chrome)
Get Tags registration.sync.getTags() Gets pending sync tags. Returns Promise<string[]>. Chrome, Edge
Unregister registration.periodicSync.unregister(tag) Unregisters periodic sync. Returns Promise. Limited (Chrome)

Example: Background Sync for retry

// In page - register background sync
async function sendMessage(message) {
  try {
    // Try sending immediately
    await fetch("/api/messages", {
      "method": "POST",
      "body": JSON.stringify(message)
    });
    console.log("Message sent");
  } catch (error) {
    // Failed - save to IndexedDB
    await saveMessageForLater(message);
    
    // Register sync to retry when online
    const registration = await navigator.serviceWorker.ready;
    await registration.sync.register("sync-messages");
    console.log("Background sync registered");
  }
}

async function saveMessageForLater(message) {
  const db = await openDatabase();
  const tx = db.transaction("pending", "readwrite");
  await tx.objectStore("pending").add(message);
}

// In service worker - handle sync event
self.addEventListener("sync", (event) => {
  console.log("Sync event:", event.tag);
  
  if (event.tag === "sync-messages") {
    event.waitUntil(syncMessages());
  }
});

async function syncMessages() {
  const db = await openDatabase();
  const messages = await db.getAll("pending");
  
  console.log(`Syncing ${messages.length} pending messages`);
  
  for (const message of messages) {
    try {
      await fetch("/api/messages", {
        "method": "POST",
        "body": JSON.stringify(message)
      });
      
      // Success - remove from pending
      await db.delete("pending", message.id);
      console.log("Message synced:", message.id);
    } catch (error) {
      // Still offline or error - will retry on next sync
      console.error("Sync failed:", error);
      throw error; // Retry sync later
    }
  }
  
  // Notify page
  await notifyAllClients({
    "type": "SYNC_COMPLETE",
    "count": messages.length
  });
}

// Check for pending syncs
async function checkPendingSyncs() {
  const registration = await navigator.serviceWorker.ready;
  const tags = await registration.sync.getTags();
  console.log("Pending syncs:", tags);
}

Example: Periodic Background Sync

// Register periodic sync (requires installed PWA)
async function registerPeriodicSync() {
  try {
    const registration = await navigator.serviceWorker.ready;
    
    // Check permission
    const status = await navigator.permissions.query({
      "name": "periodic-background-sync"
    });
    
    if (status.state === "granted") {
      // Register periodic sync (every 24 hours minimum)
      await registration.periodicSync.register("content-sync", {
        "minInterval": 24 * 60 * 60 * 1000 // 24 hours in ms
      });
      console.log("Periodic sync registered");
    }
  } catch (error) {
    console.error("Periodic sync failed:", error);
  }
}

// List periodic syncs
async function listPeriodicSyncs() {
  const registration = await navigator.serviceWorker.ready;
  const tags = await registration.periodicSync.getTags();
  console.log("Periodic syncs:", tags);
}

// Unregister periodic sync
async function unregisterPeriodicSync(tag) {
  const registration = await navigator.serviceWorker.ready;
  await registration.periodicSync.unregister(tag);
  console.log("Unregistered:", tag);
}

// In service worker - handle periodic sync
self.addEventListener("periodicsync", (event) => {
  console.log("Periodic sync event:", event.tag);
  
  if (event.tag === "content-sync") {
    event.waitUntil(syncContent());
  }
});

async function syncContent() {
  try {
    // Fetch fresh content
    const response = await fetch("/api/content");
    const data = await response.json();
    
    // Update cache
    const cache = await caches.open("content-cache");
    await cache.put("/api/content", new Response(JSON.stringify(data)));
    
    console.log("Content synced");
    
    // Notify clients
    await notifyAllClients({
      "type": "CONTENT_UPDATED",
      "timestamp": Date.now()
    });
  } catch (error) {
    console.error("Sync failed:", error);
  }
}
Note: Background Sync has limited browser support (Chrome, Edge). Periodic Sync requires installed PWA and permission. Browser controls actual sync timing - minInterval is minimum, not guaranteed. Sync events retry automatically if promise rejects.
Warning: Don't rely on Background Sync for critical operations - not supported everywhere. Periodic Sync can be delayed/skipped by browser to save battery. Always provide fallback (manual refresh button). Test offline scenarios thoroughly.

4. Push API and Push Notifications

Method Description Browser Support
registration.pushManager.subscribe(options) Subscribes to push notifications. Returns Promise<PushSubscription>. Requires permission. All Modern Browsers
registration.pushManager.getSubscription() Gets existing subscription. Returns Promise<PushSubscription | null>. All Modern Browsers
subscription.unsubscribe() Unsubscribes from push. Returns Promise<boolean>. All Modern Browsers
Notification.requestPermission() Requests notification permission. Returns Promise<"granted" | "denied" | "default">. All Browsers
PushSubscription Property Type Description
endpoint string Push service URL. Send push messages to this endpoint.
keys object Encryption keys: p256dh (public key), auth (authentication secret).
expirationTime number | null Subscription expiration timestamp. null if no expiration.

Example: Subscribe to push notifications

// Request notification permission
async function requestNotificationPermission() {
  const permission = await Notification.requestPermission();
  console.log("Notification permission:", permission);
  return permission === "granted";
}

// Subscribe to push
async function subscribeToPush() {
  try {
    // Check permission
    if (Notification.permission !== "granted") {
      const granted = await requestNotificationPermission();
      if (!granted) {
        console.log("Notification permission denied");
        return;
      }
    }
    
    // Get service worker registration
    const registration = await navigator.serviceWorker.ready;
    
    // Check existing subscription
    let subscription = await registration.pushManager.getSubscription();
    
    if (!subscription) {
      // Subscribe (need VAPID public key from server)
      subscription = await registration.pushManager.subscribe({
        "userVisibleOnly": true,
        "applicationServerKey": urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
      });
      
      console.log("Subscribed to push");
    } else {
      console.log("Already subscribed");
    }
    
    // Send subscription to server
    await sendSubscriptionToServer(subscription);
    
    return subscription;
  } catch (error) {
    console.error("Push subscription failed:", error);
  }
}

// Convert VAPID key
function urlBase64ToUint8Array(base64String) {
  const padding = "=".repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding)
    .replace(/-/g, "+")
    .replace(/_/g, "/");
  
  const rawData = atob(base64);
  const outputArray = new Uint8Array(rawData.length);
  
  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
}

// Send subscription to server
async function sendSubscriptionToServer(subscription) {
  const response = await fetch("/api/subscribe", {
    "method": "POST",
    "headers": { "Content-Type": "application/json" },
    "body": JSON.stringify(subscription)
  });
  
  if (response.ok) {
    console.log("Subscription saved on server");
  }
}

// Unsubscribe
async function unsubscribeFromPush() {
  const registration = await navigator.serviceWorker.ready;
  const subscription = await registration.pushManager.getSubscription();
  
  if (subscription) {
    await subscription.unsubscribe();
    console.log("Unsubscribed from push");
    
    // Notify server
    await fetch("/api/unsubscribe", {
      "method": "POST",
      "body": JSON.stringify({ "endpoint": subscription.endpoint })
    });
  }
}

Example: Handle push events in service worker

// In service worker - listen for push events
self.addEventListener("push", (event) => {
  console.log("Push received");
  
  let data = {
    "title": "New Notification",
    "body": "You have a new message",
    "icon": "/icon.png",
    "badge": "/badge.png"
  };
  
  // Parse push data
  if (event.data) {
    data = event.data.json();
  }
  
  // Show notification
  event.waitUntil(
    self.registration.showNotification(data.title, {
      "body": data.body,
      "icon": data.icon,
      "badge": data.badge,
      "data": data.url,
      "tag": data.tag || "default",
      "requireInteraction": false,
      "actions": [
        { "action": "open", "title": "Open" },
        { "action": "close", "title": "Close" }
      ]
    })
  );
});

// Handle notification click
self.addEventListener("notificationclick", (event) => {
  console.log("Notification clicked:", event.action);
  
  event.notification.close();
  
  if (event.action === "open") {
    // Open URL from notification data
    event.waitUntil(
      clients.openWindow(event.notification.data || "/")
    );
  } else if (event.action === "close") {
    // Just close (default behavior)
  } else {
    // Default click (no action button)
    event.waitUntil(
      clients.openWindow(event.notification.data || "/")
    );
  }
});

// Handle notification close
self.addEventListener("notificationclose", (event) => {
  console.log("Notification closed:", event.notification.tag);
  
  // Track analytics
  fetch("/api/analytics/notification-closed", {
    "method": "POST",
    "body": JSON.stringify({
      "tag": event.notification.tag,
      "timestamp": Date.now()
    })
  });
});
Note: Push API requires HTTPS and service worker. Need VAPID keys from server for applicationServerKey. Must show notification when push received (userVisibleOnly: true). Subscription can expire - check expirationTime and resubscribe.
Warning: Always request permission with user gesture (button click). Don't spam notifications - users will block. Test notification display on different platforms - varies by OS. Handle subscription expiration and errors gracefully. Safari has different push implementation (APNs).

5. Background Fetch for Large Downloads

Method Description Browser Support
registration.backgroundFetch.fetch(id, requests, options) Starts background fetch. Returns Promise<BackgroundFetchRegistration>. Limited (Chrome)
registration.backgroundFetch.get(id) Gets background fetch by ID. Returns Promise<BackgroundFetchRegistration>. Limited (Chrome)
registration.backgroundFetch.getIds() Gets all background fetch IDs. Returns Promise<string[]>. Limited (Chrome)

Example: Background fetch for offline downloads

// Start background fetch
async function downloadFiles(urls) {
  try {
    const registration = await navigator.serviceWorker.ready;
    
    const bgFetch = await registration.backgroundFetch.fetch(
      "download-videos",
      urls,
      {
        "title": "Downloading videos",
        "icons": [{ "src": "/icon.png", "sizes": "192x192" }],
        "downloadTotal": 50 * 1024 * 1024 // 50 MB estimate
      }
    );
    
    console.log("Background fetch started:", bgFetch.id);
    
    // Listen for progress
    bgFetch.addEventListener("progress", () => {
      const percent = (bgFetch.downloaded / bgFetch.downloadTotal) * 100;
      console.log(`Progress: ${percent.toFixed(2)}%`);
    });
    
    return bgFetch;
  } catch (error) {
    console.error("Background fetch failed:", error);
  }
}

// Check background fetch status
async function checkDownloadStatus(id) {
  const registration = await navigator.serviceWorker.ready;
  const bgFetch = await registration.backgroundFetch.get(id);
  
  if (bgFetch) {
    console.log(`Downloaded: ${bgFetch.downloaded} / ${bgFetch.downloadTotal}`);
    console.log(`Result: ${bgFetch.result}`); // "", "success", or "failure"
    console.log(`Failure reason: ${bgFetch.failureReason}`);
  }
}

// In service worker - handle background fetch events
self.addEventListener("backgroundfetchsuccess", (event) => {
  console.log("Background fetch succeeded:", event.registration.id);
  
  event.waitUntil(async function() {
    // Get downloaded files
    const records = await event.registration.matchAll();
    const files = await Promise.all(
      records.map(async (record) => {
        const response = await record.responseReady;
        const blob = await response.blob();
        return { "url": record.request.url, "blob": blob };
      })
    );
    
    // Cache downloaded files
    const cache = await caches.open("downloads");
    for (const file of files) {
      await cache.put(file.url, new Response(file.blob));
    }
    
    // Update UI badge
    await event.updateUI({ "title": "Download complete!" });
    
    // Show notification
    await self.registration.showNotification("Download Complete", {
      "body": `${files.length} file(s) downloaded`,
      "icon": "/icon.png"
    });
  }());
});

self.addEventListener("backgroundfetchfail", (event) => {
  console.log("Background fetch failed:", event.registration.id);
  
  event.waitUntil(
    self.registration.showNotification("Download Failed", {
      "body": "Please try again later",
      "icon": "/icon.png"
    })
  );
});

self.addEventListener("backgroundfetchabort", (event) => {
  console.log("Background fetch aborted:", event.registration.id);
});

self.addEventListener("backgroundfetchclick", (event) => {
  console.log("Background fetch UI clicked:", event.registration.id);
  
  event.waitUntil(
    clients.openWindow("/downloads")
  );
});
Note: Background Fetch has very limited support (Chrome only). Good for large file downloads that continue even if page closed. Browser shows download UI to user. Files available after download completes via backgroundfetchsuccess event.

6. Workbox Integration Patterns

Workbox Strategy Description Use Case
NetworkFirst Network request first, fallback to cache if offline. Dynamic content, API responses
CacheFirst Cache first, fallback to network if missing. Static assets, fonts, images
StaleWhileRevalidate Return cache immediately, update cache in background. Frequently updated content
NetworkOnly Always fetch from network, no caching. Analytics, real-time data
CacheOnly Only use cache, never network. Pre-cached app shell

Example: Workbox setup

// Import Workbox (in service worker)
importScripts("https://storage.googleapis.com/workbox-cdn/releases/6.5.4/workbox-sw.js");

if (workbox) {
  console.log("Workbox loaded");
  
  // Precache static assets
  workbox.precaching.precacheAndRoute([
    { "url": "/", "revision": "v1" },
    { "url": "/styles.css", "revision": "v1" },
    { "url": "/app.js", "revision": "v1" }
  ]);
  
  // Cache images - CacheFirst
  workbox.routing.registerRoute(
    ({ request }) => request.destination === "image",
    new workbox.strategies.CacheFirst({
      "cacheName": "images",
      "plugins": [
        new workbox.expiration.ExpirationPlugin({
          "maxEntries": 60,
          "maxAgeSeconds": 30 * 24 * 60 * 60 // 30 days
        })
      ]
    })
  );
  
  // Cache CSS/JS - StaleWhileRevalidate
  workbox.routing.registerRoute(
    ({ request }) => 
      request.destination === "style" ||
      request.destination === "script",
    new workbox.strategies.StaleWhileRevalidate({
      "cacheName": "static-resources"
    })
  );
  
  // API calls - NetworkFirst
  workbox.routing.registerRoute(
    ({ url }) => url.pathname.startsWith("/api/"),
    new workbox.strategies.NetworkFirst({
      "cacheName": "api-cache",
      "plugins": [
        new workbox.expiration.ExpirationPlugin({
          "maxEntries": 50,
          "maxAgeSeconds": 5 * 60 // 5 minutes
        })
      ]
    })
  );
  
  // Google Fonts - CacheFirst
  workbox.routing.registerRoute(
    ({ url }) => url.origin === "https://fonts.googleapis.com",
    new workbox.strategies.StaleWhileRevalidate({
      "cacheName": "google-fonts-stylesheets"
    })
  );
  
  workbox.routing.registerRoute(
    ({ url }) => url.origin === "https://fonts.gstatic.com",
    new workbox.strategies.CacheFirst({
      "cacheName": "google-fonts-webfonts",
      "plugins": [
        new workbox.cacheableResponse.CacheableResponsePlugin({
          "statuses": [0, 200]
        }),
        new workbox.expiration.ExpirationPlugin({
          "maxAgeSeconds": 60 * 60 * 24 * 365, // 1 year
          "maxEntries": 30
        })
      ]
    })
  );
  
  // Offline fallback
  workbox.routing.setCatchHandler(({ event }) => {
    if (event.request.destination === "document") {
      return caches.match("/offline.html");
    }
    return Response.error();
  });
  
} else {
  console.log("Workbox failed to load");
}
Note: Workbox simplifies service worker development with built-in strategies and plugins. Use workbox-cli to generate precache manifest. Workbox handles common patterns: caching, routing, expiration, background sync. Consider bundle size when using Workbox - can use module imports instead of CDN.

Service Worker Best Practices

  • Always use versioned cache names - easier to manage updates and cleanup
  • Use skipWaiting() and clients.claim() for immediate activation
  • Implement proper fetch strategies - CacheFirst for static, NetworkFirst for dynamic
  • Always include offline fallback page in precache
  • Test service worker updates - old workers can persist and cause bugs
  • Use event.waitUntil() to extend event lifetime for async operations
  • Don't cache authenticated content unless intentional - privacy/security risk
  • Implement Background Sync for offline form submissions and retries
  • Request notification permission with clear explanation and user gesture
  • Handle push subscription expiration - resubscribe when needed
  • Use Workbox for production - handles edge cases and best practices
  • Monitor service worker errors with analytics - hard to debug in production