NexusCS

Service Worker API

JavaScript
Service Workers are scripts that run in the background, enabling offline capabilities, push notifications, and background sync for web applications.
pwa
cache
offline
workers

Getting started

Service Workers act as network proxies, intercepting requests and serving cached responses for offline-first web apps.

Registration

// Check support & register
if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register("/sw.js");
}

With scope

// Register with custom scope
navigator.serviceWorker.register("/sw.js", {
  scope: "/app/",
});
// Returns: Promise<ServiceWorkerRegistration>

Registration promise

navigator.serviceWorker
  .register("/sw.js")
  .then((reg) => {
    console.log("Registered:", reg.scope);
  })
  .catch((error) => {
    console.error("Failed:", error);
  });

Lifecycle events

Install event

const CACHE = "v1";

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches
      .open(CACHE)
      .then((cache) =>
        cache.addAll(["/", "/index.html", "/style.css", "/app.js"]),
      ),
  );
});

Pre-cache critical assets during installation.

Activate event

self.addEventListener("activate", (event) => {
  const keep = ["v2"];
  event.waitUntil(
    caches
      .keys()
      .then((keys) =>
        Promise.all(
          keys.filter((k) => !keep.includes(k)).map((k) => caches.delete(k)),
        ),
      ),
  );
});

Clean up old caches on activation.

Fetch event

self.addEventListener("fetch", (event) => {
  event.respondWith(
    caches
      .match(event.request)
      .then((cached) => cached || fetch(event.request)),
  );
});

Intercept network requests.

Message event

self.addEventListener("message", (event) => {
  console.log("Message:", event.data);
  event.source.postMessage("Reply");
});

Communicate with client pages.

Caching strategies

Cache first

self.addEventListener("fetch", (event) => {
  event.respondWith(
    caches
      .match(event.request)
      .then((cached) => cached || fetch(event.request)),
  );
});

Serve from cache, fallback to network.

Network first

self.addEventListener("fetch", (event) => {
  event.respondWith(
    fetch(event.request).catch(() => caches.match(event.request)),
  );
});

Try network first, fallback to cache.

Stale while revalidate

self.addEventListener("fetch", (event) => {
  event.respondWith(
    caches.open(CACHE).then((cache) =>
      cache.match(event.request).then((cached) => {
        const fetched = fetch(event.request).then((response) => {
          cache.put(event.request, response.clone());
          return response;
        });
        return cached || fetched;
      }),
    ),
  );
});

Serve cached, update in background.

Cache only

self.addEventListener("fetch", (event) => {
  event.respondWith(caches.match(event.request));
});

Only serve from cache.

Network only

self.addEventListener("fetch", (event) => {
  event.respondWith(fetch(event.request));
});

Always fetch from network.

Cache API

Opening caches

caches.open(name);
// Returns: Promise<Cache>

Deleting caches

caches.delete(name);
// Returns: Promise<boolean>

Listing caches

caches.keys();
// Returns: Promise<string[]>

Adding to cache

cache.add(request);
// Returns: Promise<void>

Adding multiple

cache.addAll(["/index.html", "/style.css", "/app.js"]);
// Returns: Promise<void>

Storing response

cache.put(request, response);
// Returns: Promise<void>

Matching request

cache.match(request);
// Returns: Promise<Response|undefined>

Matching all

cache.matchAll(request);
// Returns: Promise<Response[]>

Deleting entry

cache.delete(request);
// Returns: Promise<boolean>

Listing entries

cache.keys();
// Returns: Promise<Request[]>

Skip waiting & claiming

Skip waiting

// In service worker
self.addEventListener("install", (event) => {
  self.skipWaiting();
});

Activate immediately without waiting.

Claim clients

self.addEventListener("activate", (event) => {
  event.waitUntil(self.clients.claim());
});

Take control of all open pages.

Combined pattern

self.addEventListener("install", (event) => {
  self.skipWaiting();
});

self.addEventListener("activate", (event) => {
  event.waitUntil(clients.claim());
});

Force immediate activation and control.

Clients API

Get all clients

self.clients.matchAll(options)
// Returns: Promise<Client[]>

// Options
{
  includeUncontrolled: false,
  type: 'window' // 'window', 'worker', 'all'
}

Get client by ID

self.clients.get(id);
// Returns: Promise<Client>

Open new window

self.clients.openWindow(url);
// Returns: Promise<WindowClient>

Claim clients

self.clients.claim();
// Returns: Promise<void>

Send message to client

client.postMessage(message);

Client properties

client.id; // string
client.type; // 'window' | 'worker'
client.url; // string
client.frameType; // 'top-level' | 'nested'

Push notifications

Subscribe from page

const reg = await navigator.serviceWorker.ready;

const sub = await reg.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: vapidPublicKey,
});

// Send subscription to server
await fetch("/subscribe", {
  method: "POST",
  body: JSON.stringify(sub),
  headers: { "Content-Type": "application/json" },
});

Subscribe to push notifications.

Listen in service worker

self.addEventListener("push", (event) => {
  const data = event.data.json();

  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: data.icon,
      badge: data.badge,
      tag: data.tag,
      data: data.url,
    }),
  );
});

Handle incoming push events.

Notification click

self.addEventListener("notificationclick", (event) => {
  event.notification.close();

  event.waitUntil(clients.openWindow(event.notification.data));
});

Handle notification clicks.

Unsubscribe

const reg = await navigator.serviceWorker.ready;
const sub = await reg.pushManager.getSubscription();

if (sub) {
  await sub.unsubscribe();
}

Unsubscribe from push notifications.

Background sync

Register sync from page

const reg = await navigator.serviceWorker.ready;

await reg.sync.register("sync-messages");

Register background sync task.

Listen in service worker

self.addEventListener("sync", (event) => {
  if (event.tag === "sync-messages") {
    event.waitUntil(sendOutboxMessages());
  }
});

Handle sync events.

Example sync function

async function sendOutboxMessages() {
  const outbox = await getOutboxMessages();

  await Promise.all(
    outbox.map((msg) =>
      fetch("/api/messages", {
        method: "POST",
        body: JSON.stringify(msg),
      }).then(() => removeFromOutbox(msg.id)),
    ),
  );
}

Process queued tasks.

ServiceWorkerRegistration

Properties

reg.installing; // ServiceWorker | null
reg.waiting; // ServiceWorker | null
reg.active; // ServiceWorker | null
reg.scope; // string

Update registration

reg.update();
// Returns: Promise<void>

Unregister

reg.unregister();
// Returns: Promise<boolean>

Push manager

reg.pushManager;
// PushManager instance

Background sync

reg.sync;
// SyncManager instance

Notifications

reg.showNotification(title, options);
reg.getNotifications(options);

Communication patterns

Page to service worker

// From page
navigator.serviceWorker.controller.postMessage({
  type: "CACHE_URLS",
  urls: ["/page1.html", "/page2.html"],
});

Send messages from page.

Service worker to page

// In service worker
self.addEventListener("message", (event) => {
  event.source.postMessage({
    type: "CACHE_COMPLETE",
  });
});

Reply to page.

Broadcast to all clients

self.clients.matchAll().then((clients) => {
  clients.forEach((client) =>
    client.postMessage({
      type: "UPDATE_AVAILABLE",
    }),
  );
});

Send to all open pages.

Page listens for messages

navigator.serviceWorker.addEventListener("message", (event) => {
  if (event.data.type === "UPDATE_AVAILABLE") {
    showUpdatePrompt();
  }
});

Receive messages in page.

Advanced caching

Cache with expiration

const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours

async function cachedFetch(request) {
  const cache = await caches.open(CACHE);
  const cached = await cache.match(request);

  if (cached) {
    const cachedTime = new Date(cached.headers.get("date"));
    const age = Date.now() - cachedTime.getTime();

    if (age < CACHE_DURATION) {
      return cached;
    }
  }

  const response = await fetch(request);
  cache.put(request, response.clone());
  return response;
}

Cache with time-based expiration.

Selective caching

self.addEventListener("fetch", (event) => {
  const url = new URL(event.request.url);

  // Only cache same-origin requests
  if (url.origin !== location.origin) {
    event.respondWith(fetch(event.request));
    return;
  }

  // Only cache GET requests
  if (event.request.method !== "GET") {
    event.respondWith(fetch(event.request));
    return;
  }

  event.respondWith(
    caches
      .match(event.request)
      .then((cached) => cached || fetch(event.request)),
  );
});

Cache only specific requests.

Cache size limits

async function trimCache(cacheName, maxItems) {
  const cache = await caches.open(cacheName);
  const keys = await cache.keys();

  if (keys.length > maxItems) {
    await cache.delete(keys[0]);
    return trimCache(cacheName, maxItems);
  }
}

Limit cache entry count.

Debugging

Chrome DevTools

Application → Service Workers
- View status (activated/waiting/installing)
- Update on reload
- Bypass for network
- Unregister

Application → Cache Storage
- Inspect cached entries
- Delete caches

Chrome debugging features.

Firefox DevTools

Application → Service Workers
- View registrations
- Start/stop workers
- Unregister

Storage → Cache Storage
- Browse caches
- Delete entries

Firefox debugging features.

Force update

navigator.serviceWorker.getRegistration().then((reg) => reg.update());

Manually check for updates.

Unregister all

navigator.serviceWorker
  .getRegistrations()
  .then((regs) => Promise.all(regs.map((reg) => reg.unregister())));

Remove all service workers.

Gotchas & limitations

HTTPS requirement

Service Workers require HTTPS in production (localhost exempt for development).

Scope limitations

Scope is determined by file location. /scripts/sw.js can only control /scripts/*. To control /, place at root or use Service-Worker-Allowed header.

Waiting period

New service worker waits until old one no longer controls clients. Use skipWaiting() + clients.claim() for immediate activation.

No DOM access

Service workers run in worker context. Use postMessage() to communicate with pages.

Opaque responses

Cross-origin requests without CORS create opaque responses. These inflate cache size (may report 7MB for 1KB file).

Cache persistence

Old caches persist unless explicitly deleted in activate event. Always clean up outdated caches.

Update checks

Browser checks for updated service worker on navigation. Force check with registration.update().

Installation failure

If any file in cache.addAll() fails to fetch, entire installation fails. Handle errors appropriately.

Also see