Featured image of post Service Workers: Advanced Offline Strategies for Web Apps

Service Workers: Advanced Offline Strategies for Web Apps

Advanced service worker offline strategies covering cache-first vs network-first, Workbox integration, background sync, periodic sync, stale-while-revalidate, and offline-first architecture.

Service workers are the foundation of offline-capable progressive web apps. With browser support now universal across Chrome, Firefox, Safari, and Edge, building resilient offline experiences is a production requirement, not a progressive enhancement. This guide covers advanced strategies for caching, synchronization, and offline-first architecture.

Service Worker Lifecycle and Fundamentals

A service worker goes through three key events: install, activate, and fetch. During installation, you pre-cache static assets. Activation handles cleanup of old caches. Every fetch request passes through the worker, giving you full control over the response.

const CACHE_NAME = "my-app-v2";

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) =>
      cache.addAll(["/", "/styles/main.css", "/scripts/app.js"])
    )
  );
});

self.addEventListener("activate", (event) => {
  event.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
    )
  );
});

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

Use self.skipWaiting() in install and clients.claim() in activate to take control of all open pages immediately after update.


Cache Strategies Deep Dive

Choosing the right caching strategy depends on the resource type, update frequency, and connectivity requirements.

StrategyBest ForTrade-off
Cache-firstStatic assets, imagesStale content until next SW update
Network-firstAPI responses, HTMLSlower when online
Stale-while-revalidateMixed contentInstant load + background refresh
Cache-onlyImmutable hashed assetsNo fallback if missing
Network-onlyReal-time data, mutationsNo offline support

Stale-while-revalidate is the recommended default for most content. It returns the cached version immediately and fetches a fresh copy in the background:

async function staleWhileRevalidate(request) {
  const cache = await caches.open("dynamic-v1");
  const cached = await cache.match(request);

  const fetchPromise = fetch(request).then((response) => {
    cache.put(request, response.clone());
    return response;
  });

  return cached || fetchPromise;
}

Handle race conditions during simultaneous page loads by using a pending request map to avoid duplicate network fetches.


Background Sync and Periodic Sync

The Background Sync API lets you defer actions until the user has stable connectivity. This is ideal for offline form submissions and queued API mutations.

// Register a sync event in the page
async function queueSubmission(data) {
  const db = await openDB("offline-queue", 1);
  await db.add("submissions", data);
  await navigator.serviceWorker.ready.then((sw) =>
      sw.sync.register("submit-data")
  );
}

// Handle sync in the service worker
self.addEventListener("sync", (event) => {
  if (event.tag === "submit-data") {
    event.waitUntil(processQueue());
  }
});

Periodic Sync goes further, allowing background updates at browser-defined intervals. Use it for pre-fetching news articles, weather data, or social media feeds. Note that Periodic Sync requires a user-granted permission and is currently limited in Safari.


Offline-First Architecture

Offline-first means designing your application so that offline is the default and connectivity is an enhancement. IndexedDB serves as the source of truth, with remote synchronization happening in the background.

interface SyncQueue {
  id: string;
  action: "create" | "update" | "delete";
  endpoint: string;
  body: unknown;
  createdAt: Date;
}

class OfflineStore {
  private db: IDBDatabase;

  async enqueue(entry: SyncQueue) {
    const tx = this.db.transaction("sync-queue", "readwrite");
    await tx.store.add(entry);
    await navigator.serviceWorker.ready.then((sw) =>
        sw.sync.register("sync-queue")
    );
  }

  async processQueue() {
    const tx = this.db.transaction("sync-queue", "readonly");
    const entries = await tx.store.getAll();
    for (const entry of entries) {
      try {
        await fetch(entry.endpoint, {
          method: "POST",
          body: JSON.stringify(entry.body),
        });
        const deleteTx = this.db.transaction("sync-queue", "readwrite");
        await deleteTx.store.delete(entry.id);
      } catch {
        break; // Stop processing if offline
      }
    }
  }
}

Conflict resolution strategies matter: last-write-wins is simple but lossy; CRDTs and operational transforms preserve user intent in collaborative scenarios. Consider frameworks like RxDB or PouchDB for built-in offline-first support with automatic replication.


Advanced Cache Management and Testing

Cache versioning prevents serving stale assets after deployment. Use a version string in your cache name and delete old caches during activation. Manage storage quotas with navigator.storage.estimate() and request persistent storage to prevent eviction:

async function requestPersistentStorage() {
  if (navigator.storage && navigator.storage.persist) {
    const granted = await navigator.storage.persist();
    console.log(`Persistent storage: ${granted ? "granted" : "denied"}`);
  }
}

For testing, Chrome DevTools offers manual offline simulation and cache inspection. Automated tests with Playwright can verify offline behavior:

import { test, expect } from "@playwright/test";

test("app works offline", async ({ page, context }) => {
  await page.goto("https://my-app.com");
  await context.setOffline(true);
  await page.reload();
  await expect(page.locator("h1")).toHaveText("My App");
});

Navigation preload reduces the performance impact of service worker bootup. Enable it in your install handler and use the preload response as a network-first fallback.

Service workers are now a production-standard tool for web reliability. Start with stale-while-revalidate for your content, add background sync for mutations, and evolve toward full offline-first as your architecture matures. The result is an application that respects users regardless of their connectivity.