Progressive Web App Expert
Build installable, offline-capable web apps with Service Workers, smart caching, and native-like experiences.
When to Use This Skill
-
Making a web app installable on mobile/desktop
-
Implementing offline functionality
-
Setting up Service Worker caching strategies
-
Handling install prompts (beforeinstallprompt )
-
Background sync for offline-first apps
-
Managing PWA update flows
-
Creating web app manifests
When NOT to Use This Skill
-
Native app development → Use React Native, Flutter, or native SDKs
-
General web performance → Use Lighthouse/performance auditing tools
-
Server-side rendering issues → Use Next.js/framework-specific docs
-
Push notifications only → Consider dedicated push notification services
-
Simple static sites → PWA overhead may not be worth it
Core Concepts
What Makes a PWA Installable
-
HTTPS (or localhost for dev)
-
Web App Manifest with required fields
-
Service Worker with fetch handler
-
Icons (192×192 and 512×512 minimum)
The PWA Stack
┌─────────────────────────────────────────┐ │ Your App (React/Next.js) │ ├─────────────────────────────────────────┤ │ Service Worker (sw.js) │ │ ┌─────────────┐ ┌─────────────────┐ │ │ │ Cache │ │ Network Fetch │ │ │ │ Storage │ │ Handling │ │ │ └─────────────┘ └─────────────────┘ │ ├─────────────────────────────────────────┤ │ manifest.json │ │ (App identity, icons, display mode) │ └─────────────────────────────────────────┘
Web App Manifest
Complete manifest.json
{ "name": "Junkie Buds 4 Life", "short_name": "JB4L", "description": "Recovery support app", "start_url": "/", "scope": "/", "display": "standalone", "orientation": "portrait-primary", "background_color": "#1a1410", "theme_color": "#1a1410", "icons": [ { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { "src": "/icons/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ], "shortcuts": [ { "name": "Find Meetings", "short_name": "Meetings", "url": "/meetings?source=shortcut", "icons": [{ "src": "/icons/meetings-96.png", "sizes": "96x96" }] } ] }
Display Modes
Mode Description
fullscreen
No browser UI, full screen
standalone
App-like, no URL bar (recommended)
minimal-ui
Some browser controls
browser
Normal browser tab
Link in HTML
<head> <link rel="manifest" href="/manifest.json" /> <meta name="theme-color" content="#1a1410" /> <meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" /> </head>
Service Worker Basics
Registration
// lib/pwa.ts export async function registerServiceWorker() { if ('serviceWorker' in navigator) { try { const registration = await navigator.serviceWorker.register('/sw.js', { scope: '/', }); return registration; } catch (error) { console.error('SW registration failed:', error); } } }
// Call on app mount useEffect(() => { registerServiceWorker(); }, []);
Basic Service Worker Structure
// public/sw.js const CACHE_NAME = 'myapp-v1'; const STATIC_ASSETS = ['/', '/offline', '/manifest.json'];
// Install: Cache static assets self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS)) ); self.skipWaiting(); });
// Activate: Clean old caches self.addEventListener('activate', (event) => { event.waitUntil( caches.keys().then((keys) => Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))) ) ); self.clients.claim(); });
// Fetch: Handle requests (see references for strategies) self.addEventListener('fetch', (event) => { event.respondWith(handleFetch(event.request)); });
See: references/service-worker-patterns.md for caching strategy implementations
Caching Strategies
Strategy Best For Tradeoff
Cache-First Static assets, fonts, images Stale until cache updated
Network-First API data, user content Slower, needs connectivity
Stale-While-Revalidate Balance freshness/speed Background updates
Network-Only Auth, real-time data No offline support
Cache-Only Versioned assets Never updates
See: references/service-worker-patterns.md for full implementations
Install Prompts
Handle the beforeinstallprompt event to show a custom install UI:
// Basic pattern const [deferredPrompt, setDeferredPrompt] = useState(null);
useEffect(() => { window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); setDeferredPrompt(e); }); }, []);
const handleInstall = async () => { if (deferredPrompt) { deferredPrompt.prompt(); const { outcome } = await deferredPrompt.userChoice; // outcome: 'accepted' or 'dismissed' } };
See: references/install-prompt.md for full usePWAInstall hook and component
Offline Experience
Key patterns:
-
Offline page fallback for navigation failures
-
useOnlineStatus hook to detect connectivity
-
Offline banner to inform users
See: references/offline-handling.md for implementations
Background Sync
Queue actions while offline, execute when connectivity returns:
// In Service Worker self.addEventListener('sync', (event) => { if (event.tag === 'sync-data') { event.waitUntil(syncPendingData()); } });
// In App - trigger sync const registration = await navigator.serviceWorker.ready; await registration.sync.register('sync-data');
See: references/background-sync.md for full IndexedDB integration
Update Flow
Notify users when a new version is available:
// Basic pattern registration.addEventListener('updatefound', () => { const newWorker = registration.installing; newWorker?.addEventListener('statechange', () => { if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { // New version available - show update prompt } }); });
See: references/update-flow.md for usePWAUpdate hook and update strategies
Next.js Integration
Options for Next.js PWA:
-
next-pwa - Works with standard Next.js server
-
Custom SW - Required for output: 'export' (static sites)
-
Workbox CLI - Generate SW after build
See: references/nextjs-integration.md for detailed configurations
Quick Reference
Task Solution
Check if installed window.matchMedia('(display-mode: standalone)').matches
Force SW update registration.update()
Clear all caches caches.keys().then(keys => keys.forEach(k => caches.delete(k)))
Check online navigator.onLine
Get SW registration navigator.serviceWorker.ready
Skip waiting self.skipWaiting() in SW
Take control self.clients.claim() in SW
Testing PWA
Chrome DevTools
-
Application tab → Manifest, Service Workers, Cache Storage
-
Lighthouse → PWA audit
-
Network → Offline checkbox to simulate
Debug Checklist
-
Manifest loads (Application → Manifest)
-
SW registered (Application → Service Workers)
-
Cache populated (Application → Cache Storage)
-
Install prompt fires (Console for beforeinstallprompt)
-
Offline page works (Network → Offline)
-
Update flow works (trigger update, verify prompt)
References
Detailed implementations in /references/ :
-
service-worker-patterns.md
-
Caching strategy implementations
-
install-prompt.md
-
usePWAInstall hook and install component
-
offline-handling.md
-
Offline page, status hooks, banners
-
background-sync.md
-
Background sync with IndexedDB
-
update-flow.md
-
Update detection and user prompts
-
nextjs-integration.md
-
Next.js PWA configuration options