Skip to content
fearchitect
Quality, Observability & Cross-Cutting

PWA & Offline

Installable, offline-capable web apps via service workers and manifests.

By Abas TurabliReviewed

Summary

A Progressive Web App adds installability (Web App Manifest + icons) and offline capability (service worker caching) on top of a normal web site. The service worker intercepts fetch events and serves cached responses when the network is unavailable. Workbox or Serwist handle the boilerplate; IndexedDB stores structured data offline.

Jump to the interview angle

Progressive Web App (PWA)

A web app that meets Chrome's installability criteria — a valid Web App Manifest with a 192 × 192 icon, an HTTPS origin, and a registered service worker — so the browser offers an 'Add to Home Screen' prompt. The service worker (SW) is a background JS thread that intercepts all outgoing fetches, enabling offline responses, push notifications, and background sync. A manifest alone doesn't make a site offline-capable; the SW's fetch handler does.

Service worker lifecycle

  1. 1

    Register

    The page calls navigator.serviceWorker.register('/sw.js'). The browser downloads the SW file and begins the install phase. An existing active SW continues serving the page until the new one takes control.

  2. 2

    Install

    The SW's install event fires. Pre-cache shell assets here with cache.addAll(urls). Call event.waitUntil() to delay activation until caching succeeds. Failure rolls back to the previous SW.

  3. 3

    Activate

    Fires after install succeeds and no old SW is controlling pages. Delete stale caches here. Call self.clients.claim() to immediately take control of open tabs without waiting for a reload.

  4. 4

    Fetch

    Every network request from controlled pages hits the SW's fetch event. Respond with event.respondWith(strategy(request)) — return a cached response, a network response, or a fallback.

  5. 5

    Update

    The browser checks the SW file on every navigation. A byte-changed file triggers a new install. The new SW waits in waiting state unless self.skipWaiting() is called, which forces immediate activation.

SW caching strategies

StrategyResponse sourceNetwork hit?Best for
Cache firstCache; falls back to network and updates cacheOnly on cache missVersioned static assets, fonts, icons
Network firstNetwork; falls back to cache on failureAlways attemptedAPI responses that must be fresh when online
Stale-while-revalidateCache immediately; fetches network in background to refresh cacheYes, in backgroundNon-critical assets where speed matters more than freshness
Network onlyNetwork only; no cache read or writeAlwaysAnalytics pings, POST mutations
Cache onlyCache only; fails if not cachedNeverPre-cached shell assets with no fallback needed

Serwist fetch handler with stale-while-revalidate

Serwist (the Workbox fork maintained by @serwist/sw) ships typed strategy classes. Register routes in the SW entry point — Vite and Next.js plugins auto-inject the precache manifest.

SW entry point using Serwist StaleWhileRevalidatets
// sw.ts — compiled by Serwist's Vite/Next plugin
import { Serwist } from "serwist";
import { StaleWhileRevalidate, CacheFirst } from "serwist";
import { ExpirationPlugin } from "serwist";

const serwist = new Serwist({
  precacheEntries: self.__SW_MANIFEST, // injected at build time
  skipWaiting: true,
  clientsClaim: true,
  navigationPreload: true,
});

// Versioned JS/CSS bundles: cache-first, auto-expire after 30 days
serwist.registerRoute(
  ({ request }) => request.destination === "script" || request.destination === "style",
  new CacheFirst({
    cacheName: "static-assets",
    plugins: [new ExpirationPlugin({ maxAgeSeconds: 60 * 60 * 24 * 30 })],
  }),
);

// HTML navigation: stale-while-revalidate so shell loads fast
serwist.registerRoute(
  ({ request }) => request.mode === "navigate",
  new StaleWhileRevalidate({ cacheName: "pages" }),
);

serwist.addEventListeners();

ExpirationPlugin auto-purges old entries so the cache doesn't grow unbounded. navigationPreload sends the network request in parallel with SW boot, cutting response time on fast connections.

skipWaiting races with open tabs

Calling skipWaiting forces the new SW to activate immediately, but tabs still open on the old version now run new SW code against old page assets — a version mismatch. The safe pattern: prompt the user ('Update available — reload?') and call skipWaiting only after they confirm, then reload all clients with clients.matchAll() + client.navigate(client.url).

Interview angle

Interviewers probe caching strategy selection and the update hazard from skipWaiting. Know when each strategy fits and what clientsClaim does. Soundbite: "Cache-first maximises speed but serves stale content; stale-while-revalidate gives speed without stale risk for assets that tolerate a one-request lag."

Key terms

Service worker
Background JS thread that intercepts fetches and enables offline responses and push notifications.
Web App Manifest
JSON file declaring app name, icons, display mode, and start URL, required for installability.
Cache Storage API
Browser API for storing Request/Response pairs; the SW reads and writes this for caching strategies.
skipWaiting
SW call that forces the waiting SW to activate immediately, bypassing the waiting state.
IndexedDB
Browser key-value object store for structured offline data; survives SW restarts unlike in-memory state.

Further reading

Search fearchitect

Jump to a topic, mode, or action.