Progressive Web Apps (PWA) Cheat Sheet
Covers the web app manifest, service worker registration and caching strategies, install prompt handling, and a PWA feature checklist.
Web App Manifest
Declaring how the app installs and launches.
{ "name": "SkillVeris", "short_name": "SkillVeris", "start_url": "/", "display": "standalone", "background_color": "#ffffff", "theme_color": "#1976d2", "icons": [ { "src": "/icons/192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/icons/512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" } ]}
Service Worker Registration & Caching
Registering a worker and caching assets for offline use.
// Register from your appif ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js');}// sw.js - cache-first strategy for static assetsconst CACHE_NAME = 'app-v1';const ASSETS = ['/', '/index.html', '/styles.css', '/app.js'];self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS)) );});self.addEventListener('fetch', (event) => { event.respondWith( caches.match(event.request).then((cached) => cached || fetch(event.request)) );});self.addEventListener('activate', (event) => { event.waitUntil( caches.keys().then((keys) => Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))) ) );});
Install Prompt Handling
Deferring and triggering the native install UI.
let deferredPrompt;window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); // stop the automatic mini-infobar deferredPrompt = e; showCustomInstallButton();});installButton.addEventListener('click', async () => { deferredPrompt.prompt(); const { outcome } = await deferredPrompt.userChoice; // 'accepted' | 'dismissed' deferredPrompt = null;});
PWA Feature Checklist
The building blocks that make an app installable and offline-capable.
- Web App Manifest- JSON file declaring name, icons, and display mode for installability
- Service Worker- a script that intercepts network requests and enables offline support
- HTTPS requirement- service workers only register on secure origins, or on localhost
- display: standalone- launches without browser chrome so the app feels native
- Push notifications- delivered via the Push API and Notification API, requires a service worker
- Background Sync- defers actions like a form submission until connectivity returns
- Lighthouse PWA audit- checks installability, offline support, and PWA best practices
Stale-While-Revalidate & Network-First with Timeout
Production-grade fetch strategies that balance freshness against offline resilience.
// Stale-while-revalidate: serve cache instantly, refresh in backgroundasync function staleWhileRevalidate(request) { const cache = await caches.open('runtime-v1'); const cached = await cache.match(request); const networkFetch = fetch(request).then((response) => { cache.put(request, response.clone()); return response; }); return cached || networkFetch;}// Network-first with timeout fallback (good for API calls)async function networkFirstWithTimeout(request, timeoutMs = 3000) { const cache = await caches.open('api-v1'); try { const response = await Promise.race([ fetch(request), new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeoutMs)), ]); cache.put(request, response.clone()); return response; } catch (err) { const cached = await cache.match(request); if (cached) return cached; throw err; }}self.addEventListener('fetch', (event) => { const url = new URL(event.request.url); if (url.pathname.startsWith('/api/')) { event.respondWith(networkFirstWithTimeout(event.request)); } else { event.respondWith(staleWhileRevalidate(event.request)); }});
Background Sync for Offline Writes
Queueing a failed POST in IndexedDB and replaying it once connectivity returns.
// In the page: intercept a failed submit and register a syncasync function submitForm(data) { try { await fetch('/api/orders', { method: 'POST', body: JSON.stringify(data) }); } catch (err) { await queueRequest(data); // store in IndexedDB const reg = await navigator.serviceWorker.ready; await reg.sync.register('sync-orders'); // ask the browser to retry later }}// sw.jsself.addEventListener('sync', (event) => { if (event.tag === 'sync-orders') { event.waitUntil(replayQueuedOrders()); }});async function replayQueuedOrders() { const queued = await getQueuedRequests(); // read from IndexedDB for (const item of queued) { const res = await fetch('/api/orders', { method: 'POST', body: JSON.stringify(item.data) }); if (res.ok) await deleteQueuedRequest(item.id); }}
Push API Subscription & Notification Display
Subscribing a client to push and rendering the payload from the service worker.
// Page: subscribe using the VAPID public keyasync function subscribeToPush(vapidPublicKey) { const reg = await navigator.serviceWorker.ready; const subscription = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(vapidPublicKey), }); await fetch('/api/push/subscribe', { method: 'POST', body: JSON.stringify(subscription), });}// sw.js: receive push and show a notificationself.addEventListener('push', (event) => { const data = event.data ? event.data.json() : { title: 'Update', body: '' }; event.waitUntil( self.registration.showNotification(data.title, { body: data.body, icon: '/icons/192.png', badge: '/icons/badge.png', data: { url: data.url }, }) );});self.addEventListener('notificationclick', (event) => { event.notification.close(); event.waitUntil(clients.openWindow(event.notification.data.url));});
Prompting Users When a New Service Worker Is Ready
Detecting a waiting worker and letting the user opt into activation via skipWaiting + postMessage.
// Page: listen for a new SW version waiting to activatenavigator.serviceWorker.register('/sw.js').then((reg) => { reg.addEventListener('updatefound', () => { const newWorker = reg.installing; newWorker.addEventListener('statechange', () => { if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { showUpdateToast(() => newWorker.postMessage({ type: 'SKIP_WAITING' })); } }); });});let refreshing = false;navigator.serviceWorker.addEventListener('controllerchange', () => { if (refreshing) return; refreshing = true; window.location.reload();});// sw.js: honor the skip-waiting request from the pageself.addEventListener('message', (event) => { if (event.data?.type === 'SKIP_WAITING') self.skipWaiting();});
Caching Strategy Vocabulary
Terms used across PWA tooling (including Workbox) to describe fetch-handling behavior.
- Cache First- serve from cache, only hit network on a cache miss; ideal for versioned static assets
- Network First- try the network, fall back to cache on failure/timeout; good for frequently updated content
- Stale-While-Revalidate- return cache immediately, refresh the cache entry in the background for next time
- Cache Only- never hit the network; used for assets guaranteed to be precached at install time
- Network Only- always bypass the cache; used for non-idempotent requests like POST/PUT
- Precaching- caching a fixed asset manifest during the install event, before the app is first used offline
- Runtime caching- caching responses lazily as requests occur, rather than up front
- Workbox- Google's library that generates precache manifests and wraps these strategies as routable handlers
A cache-first strategy is great for versioned static assets but dangerous for API responses: use a stale-while-revalidate or network-first strategy for dynamic data so users don't get stuck seeing stale content indefinitely.