On this page · sections
Progressive Web Apps
A PWA is a web app that is installable, reliable on flaky networks, and engaging — progressive enhancement on the open web, not a separate platform.
| Piece | Role | Requirement |
|---|---|---|
| Manifest | Install metadata, icons, theme | Linked from HTML |
| Service worker | Intercepts fetch, enables offline | HTTPS (localhost exempt in dev) |
| HTTPS | Secure origin | Required for SW registration |
| Caching strategy | Per-asset freshness rules | Workbox recommended |
PWA Requirements
Definition
{
"name": "Acme Dashboard",
"short_name": "Acme",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#0f172a",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
]
}<link rel="manifest" href="/manifest.webmanifest" />
<meta name="theme-color" content="#0f172a" />if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js', { scope: '/' });
});
}HTTPS → manifest → service worker registered → install prompt eligible → SW intercepts fetchInstall prompt fires when manifest + SW + icons meet browser criteria plus engagement heuristics (visited twice, minutes apart).
display: standalone — OS status bar, app-like feel. fullscreen — hides browser chrome entirely.
iOS supports Add to Home Screen with limitations — smaller cache quotas, historically weaker push support.
Interview: PWA is manifest + service worker + HTTPS — not a framework feature.
Service Worker Caching Strategies
Definition
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst, StaleWhileRevalidate, NetworkOnly } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
precacheAndRoute(self.__WB_MANIFEST);
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images',
plugins: [new ExpirationPlugin({ maxEntries: 60, maxAgeSeconds: 30 * 24 * 3600 })],
})
);
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new NetworkFirst({ cacheName: 'api', networkTimeoutSeconds: 5 })
);
registerRoute(
({ request }) => request.method !== 'GET',
new NetworkOnly()
);| Asset | Strategy | Why |
|---|---|---|
| Hashed JS/CSS | Cache-first | Immutable filename |
index.html | Network-first | Must get latest shell |
| Product images | SWR | Fast paint, background refresh |
| POST /checkout | Network-only | Never cache mutations |
- Cache-first — read cache, fallback network — static hashed assets
- Network-first — try network, fallback cache — HTML and API GET
- Stale-while-revalidate — return cache, update in background — fonts, avatars
- Network-only — auth and mutations
Version cache names (static-v3) and delete old caches on SW activate. Never cache-first index.html — users stuck on old app version.
Interview:
app.a1b2c3.jsis immutable — cache-first safe.index.htmlis not.
Offline UX Patterns
Definition
const OFFLINE_URL = '/offline.html';
self.addEventListener('install', (event) => {
event.waitUntil(caches.open('offline-v1').then((c) => c.add(OFFLINE_URL)));
});
self.addEventListener('fetch', (event) => {
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request).catch(() => caches.match(OFFLINE_URL))
);
}
});'use client';
import { useEffect, useState } from 'react';
export function OfflineBanner() {
const [offline, setOffline] = useState(!navigator.onLine);
useEffect(() => {
const on = () => setOffline(false);
const off = () => setOffline(true);
window.addEventListener('online', on);
window.addEventListener('offline', off);
return () => {
window.removeEventListener('online', on);
window.removeEventListener('offline', off);
};
}, []);
if (!offline) return null;
return (
<div role="status">You are offline. Changes will sync when reconnected.</div>
);
}Patterns:
- Offline fallback page — precached HTML for navigate requests
- Read-only mode — show cached data, disable mutations
- Queued mutations — Background Sync API or IndexedDB queue, replay on
online - Clear messaging — banner, not silent failure
Don’t cache API responses with auth headers in shared cache — private data risk. Align SW TTL with CDN Cache-Control philosophy.
Interview: Offline UX needs explicit fallback + user messaging — not just caching assets.
Service Worker Update Lifecycle
Definition
// sw.js — new version waiting
self.addEventListener('install', (event) => {
self.skipWaiting(); // activate immediately (use with care)
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter((k) => k !== 'static-v3').map((k) => caches.delete(k)))
).then(() => clients.claim())
);
});'use client';
useEffect(() => {
if (!('serviceWorker' in navigator)) return;
navigator.serviceWorker.addEventListener('controllerchange', () => {
window.location.reload(); // new SW took control
});
navigator.serviceWorker.ready.then((reg) => {
reg.addEventListener('updatefound', () => {
const newWorker = reg.installing;
newWorker?.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// Show "Update available" prompt — don't force skipWaiting silently
}
});
});
});
}, []);Default: new SW waits until all tabs close. skipWaiting() + clients.claim() activates immediately — can cause version mismatch mid-session.
Staff UX: prompt user to refresh when update available. Coordinate with lazy chunk hashes — stale SW serving old remoteEntry causes chunk 404 after deploy.
Workbox generateSW vs injectManifest — precache manifest from build output.
Interview: Forced
skipWaitingwithout user prompt can break in-flight sessions — show update banner instead.
Production Trade-offs
Definition
PWAs add complexity — evaluate ROI per product:
| Benefit | Cost |
|---|---|
| Install to home screen | SW update lifecycle bugs |
| Offline read access | Cache invalidation complexity |
| Push notifications (where supported) | iOS limitations |
| Faster repeat visits | Storage quota management |
When PWA makes sense: field apps (retail, logistics), content readers, tools used daily on mobile with flaky connectivity.
When to skip: SEO-critical marketing sites needing instant freshness, apps with heavy real-time data where stale cache is dangerous, teams without SW ops experience.
Lighthouse PWA score checks baseline; Chrome adds engagement heuristics beyond Lighthouse.
Storage quota: navigator.storage.estimate() — prune caches with ExpirationPlugin. Test on low-end Android devices.
Staff: monitor SW registration rate, cache hit ratio, offline error rate, update prompt acceptance. Feature flag SW registration — disable if post-deploy incident.
Interview: PWA is progressive enhancement — measure whether install + offline justify SW operational cost.
Senior Interview Summary
Requirements
HTTPS + manifest + service worker. Icons 192/512. Install heuristics beyond Lighthouse.
Caching
Cache-first hashed assets. Network-first HTML/API. Network-only mutations. Workbox in production.
Offline
Fallback page, read-only mode, mutation queue, user-visible banner.
Updates
New SW waits by default. Prompt before skipWaiting. Purge old caches on activate.
Trade-offs
ROI for field/daily-use apps. Skip when freshness critical or team lacks SW ops.
Best mental model
SW = programmable proxy between app and network
Manifest = install metadata
Strategy = per-asset freshness contractPractical rules
index.html → network-first always
Hashed bundles → cache-first + immutable CDN headers
Auth responses → never cache in SW
Deploy → version cache names + user update promptWhat to say in interviews
Explain manifest + SW + HTTPS triangle, compare three caching strategies with asset examples, describe offline UX layers, and discuss update lifecycle without forcing silent skipWaiting.