# PWA & Offline > Installable, offline-capable web apps via service workers and manifests. Frontend architecture guidance from fearchitect, written by Abas Turabli and last reviewed 2026-06-21. Source: https://fearchitect.com/topics/pwa-offline Use it as reference for the task at hand. Before changing code, check this guidance against the codebase: where the code already makes a different, deliberate choice, flag the conflict instead of rewriting it. Library APIs move faster than this guide, so confirm exact signatures in the official docs linked at the end. ## 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. ## 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. 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. 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. 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. 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. 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 | Strategy | Response source | Network hit? | Best for | | --- | --- | --- | --- | | **Cache first** | Cache; falls back to network and updates cache | Only on cache miss | Versioned static assets, fonts, icons | | **Network first** | Network; falls back to cache on failure | Always attempted | API responses that must be fresh when online | | **Stale-while-revalidate** | Cache immediately; fetches network in background to refresh cache | Yes, in background | Non-critical assets where speed matters more than freshness | | **Network only** | Network only; no cache read or write | Always | Analytics pings, POST mutations | | **Cache only** | Cache only; fails if not cached | Never | Pre-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 StaleWhileRevalidate** ```ts // 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. ## Watch out: 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)`. ## 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. ## Related topics - [Caching Strategies](https://fearchitect.com/topics/caching-strategies.md): Layer browser, CDN, and app caches to serve responses without re-fetching. - [Network Performance](https://fearchitect.com/topics/network-performance.md): Hint, prioritize, and pre-navigate to cut request latency. - [Error Boundaries & Resilience](https://fearchitect.com/topics/error-boundaries-resilience.md): Isolate render failures so one widget can't crash the page. - [A/B Testing & Experimentation](https://fearchitect.com/topics/ab-testing-experimentation.md): Ship variants without flicker using server-side bucketing and guardrail metrics. - [Accessibility (a11y)](https://fearchitect.com/topics/accessibility.md): WCAG 2.2 AA: semantic HTML, keyboard nav, and ARIA done right. - [Frontend Observability](https://fearchitect.com/topics/frontend-observability.md): Capture errors, measure real-user performance, and trace what breaks in production. ## Further reading - [web.dev — Progressive Web Apps](https://web.dev/explore/progressive-web-apps) - [MDN — Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) - [Serwist documentation](https://serwist.pages.dev/docs/serwist) - [web.dev — Offline cookbook](https://web.dev/articles/offline-cookbook) - [MDN — Using IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB)