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.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
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. Test in a fresh environment.
- Accessibility audit fails: add labels, landmarks, focus management, and color contrast. Use a screen reader for manual verification.
- Hydration mismatch: ensure server and client render the same initial HTML. Avoid using Date, Math.random, or window during SSR.
FAQ
Do PWAs work on iOS? Yes, but with limitations: Safari supports service workers and add-to-home-screen, but push notifications are only available in iOS 16.4+ for installed PWAs.
How much code does a PWA add? The service worker and manifest are typically under 5KB. Third-party libraries like Workbox add ~20KB but handle edge cases for you.
Can a PWA replace my native app? For content-focused or moderately interactive apps, yes. For games, heavy AR/VR, or deep hardware integration, native apps still have advantages.
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.
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.
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.