Progressive Web Apps (PWA) — Complete Guide
A thorough guide to building Progressive Web Apps: service workers, offline support, Web App Manifest, push notifications, and installability.
Overview
Progressive Web Apps (PWA) use modern web capabilities to deliver an app-like experience: offline access, push notifications, home screen installation, and background sync. Unlike native apps, they run in the browser and require no app store approval. The following guide covers the core technologies and what works for building production PWAs.
When to Use
-
For alternatives, see JavaScript Service Worker Offline Caching for PWA.
-
You need offline functionality for a web application
-
You want to reduce app store friction while providing native-like UX
-
Your users are on mobile with intermittent connectivity
-
You need push notifications without building separate native apps
-
You want to improve engagement with add-to-home-screen prompts
Core Technologies
| Technology | Purpose | Key Capability |
|---|---|---|
| Service Worker | Background proxy for network requests | Offline caching, background sync |
| Web App Manifest | Describes app metadata for installation | Icons, display mode, theme color |
| HTTPS | Secure origin requirement for PWA capabilities | Required for service workers |
| Push API | Server-initiated messages | Re-engage users even when app is closed |
| Background Sync | Defer actions until connectivity returns | Queue form submissions offline |
Service Workers
Registration
Register the service worker at app startup.
// main.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js');
console.log('SW registered:', registration.scope);
} catch (error) {
console.error('SW registration failed:', error);
}
});
}
Cache Strategies
Choose the right strategy for each resource type.
// sw.js — Cache-First for static assets
self.addEventListener('fetch', (event) => {
if (event.request.destination === 'image') {
event.respondWith(cacheFirst(event.request));
} else if (event.request.destination === 'document') {
event.respondWith(networkFirst(event.request));
}
});
async function cacheFirst(request) {
const cached = await caches.match(request);
return cached || fetch(request).then(response => {
caches.open('v1').then(cache => cache.put(request, response.clone()));
return response;
});
}
async function networkFirst(request) {
try {
const networkResponse = await fetch(request);
const cache = await caches.open('v1');
cache.put(request, networkResponse.clone());
return networkResponse;
} catch {
return caches.match(request);
}
}
Cache Strategies Summary
| Strategy | Best For | Behavior |
|---|---|---|
| Cache First | Static assets (CSS, JS, images) | Serve from cache; fall back to network |
| Network First | HTML documents, API calls | Try network first; fall back to cache |
| Stale-While-Revalidate | Frequently updated content | Serve cached version; refresh in background |
| Network Only | Real-time data (chat, stock prices) | Always fetch from network |
| Cache Only | Pre-cached app shell | Never hit the network |
Web App Manifest
The manifest enables add-to-home-screen and defines the app experience.
{
"name": "Task Manager Pro",
"short_name": "Tasks",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#3b82f6",
"icons": [
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" }
],
"screenshots": [
{ "src": "/screenshot-1.png", "sizes": "1280x720", "type": "image/png", "form_factor": "wide" },
{ "src": "/screenshot-2.png", "sizes": "750x1334", "type": "image/png", "form_factor": "narrow" }
]
}
Offline Experience
Offline Page Pattern
Show a custom offline page instead of the browser default.
// sw.js
const OFFLINE_PAGE = '/offline.html';
self.addEventListener('fetch', (event) => {
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request).catch(() => caches.match(OFFLINE_PAGE))
);
}
});
Background Sync
Queue actions performed offline and retry when connectivity returns.
// Queue a background sync
async function submitForm(data) {
try {
await fetch('/api/submit', { method: 'POST', body: JSON.stringify(data) });
} catch {
// Save to IndexedDB and register for sync
await db.syncQueue.add(data);
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('submit-form');
}
}
// Service Worker handles the sync event
self.addEventListener('sync', (event) => {
if (event.tag === 'submit-form') {
event.waitUntil(processSyncQueue());
}
});
Push Notifications
Subscribe to Push
async function subscribeToPush() {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
});
// Send subscription to server
await fetch('/api/push-subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription)
});
}
Display Notifications
// sw.js
self.addEventListener('push', (event) => {
const data = event.data.json();
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icon-192.png',
badge: '/badge-72.png',
data: { url: data.url },
actions: [
{ action: 'open', title: 'Open App' },
{ action: 'dismiss', title: 'Dismiss' }
]
})
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action === 'open' || !event.action) {
event.waitUntil(clients.openWindow(event.notification.data.url));
}
});
Install Prompt
Prompt users to install the PWA on supported browsers.
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
showInstallButton();
});
async function installPWA() {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
console.log('User installed PWA');
}
deferredPrompt = null;
}
Testing and Debugging
| Tool | Purpose |
|---|---|
| Chrome DevTools > Application | Inspect service workers, manifests, caches |
| Lighthouse | Audit PWA compliance |
| WebPageTest | Test offline behavior on real devices |
| ngrok | Test HTTPS-required capabilities locally |
| Workbox | Library for simplifying service worker patterns |
Common Mistakes
- Not handling cache updates — users may see stale content indefinitely
- Caching API responses without versioning — stale data after deployments
- Ignoring storage quotas — browsers may evict your cache
- Over-caching live content — use Network First for user-specific data
- Missing HTTPS — service workers and push require a secure origin
Troubleshooting
- Component does not re-render: verify state reference, props, and memoization. A mutated object can bypass change detection.
- Style does not apply in production: check that CSS is loaded, class names are not mangled, and specificity wins. Purge unused styles carefully.
- Build fails after dependency update: read the changelog, pin versions, and clean the lock file.
- Accessibility audit fails: add labels, landmarks, focus management, and color contrast.
- Hydration mismatch: ensure server and client render the same initial HTML. random, or window during SSR.
Quick Reference
- Main command: run the base solution from the article and verify the expected result.
- Validation: confirm tests pass and key metrics did not degrade.
- Rollback: if something fails, revert the change and consult the Troubleshooting section.
Further Reading
- Official documentation: check the current reference for the framework or tool used.
- Related guides: explore the pwa and service-worker guides for deeper coverage.
- Complementary patterns: review design patterns applicable to your technology stack.
- Public postmortems: study real incidents from teams that faced similar production issues.
Production Notes
- Deploy gradually using canary or blue-green to catch regressions early.
- Configure alerts for error rate, p99 latency, and failure rate before enabling in production.
- Document the rollback in the runbook; test the procedure in staging at least once per quarter.
- Review structured logs with correlation IDs to trace requests end-to-end during incidents.
Key Takeaways
- Apply progressive web apps (pwa) — complete guide when you need a practical solution for your use case.
- Monitor performance after implementation; measure latency, errors, and resource usage before and after.
- Check the Troubleshooting section for common failures; most have documented root causes with fixes.
- Keep dependencies updated and run tests in CI to prevent production regressions.
Common Production Pitfalls
- Treating the guide as a checklist to complete once rather than a practice to evolve.
- Adopting every recommendation at once instead of starting with one measured change.
- Skipping the maturity assessment and forcing advanced practices on an unprepared team.
- Not updating runbooks and on-call expectations as new practices are introduced.
- Ignoring real incident data when prioritizing which parts of the guide to apply first.
- Failing to assign an owner who reviews decisions quarterly.
- Copying examples without adapting them to the team’s actual tooling and constraints.
- Forgetting to measure outcomes before adding the next improvement.
Frequently Asked Questions
How do I get started with this in an existing project?
Start with a small, isolated part of your codebase. Apply the concepts from this guide to one module or service. Measure the impact, then expand to other areas.
What tools do I need?
The tools mentioned throughout this guide are listed in each section. Most are open-source and widely adopted. Check the related resources for setup instructions.
How do I measure success after implementing this?
Define clear metrics before starting: performance benchmarks, error rates, or maintainability indicators. Compare before and after. Iterate based on the data, not on assumptions.
Related Resources
WCAG 2.2 Accessibility: A Developer Guide
A practical guide to WCAG 2.2 compliance: perceivable, operable, understandable, and reliable principles with code examples for web accessibility.
GuideWeb Components — Custom Elements, Shadow DOM & Templates
A practical guide to Web Components: creating reusable custom elements, encapsulating styles with Shadow DOM, and composing with HTML templates.
PatternCircuit Breaker Pattern
Prevent cascading failures by stopping requests to failing services. An architectural pattern for resilient distributed systems.