A Progressive Web App (PWA) is a web app with native-app-like features: offline support, push notifications, installable on home screen, background sync. Built using web standards: service workers, manifests, web APIs.
The PWA promise was that one codebase replaces native apps. The reality is more nuanced.
A JavaScript worker that runs separately from your page. It can intercept network requests, cache responses, work offline.
// Register
navigator.serviceWorker.register('/sw.js');
// In sw.js
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
);
});
Service workers enable:
A JSON file describing the app:
{
"name": "My App",
"short_name": "MyApp",
"start_url": "/",
"display": "standalone",
"icons": [...]
}
The browser uses this for "install to home screen" — the app gets its own icon and runs in a standalone window.
Programmatic control over what's cached. Used inside service workers.
For server-to-client push notifications via a push service (Firebase Cloud Messaging, etc.).
Cached resources serve when network is down. For content-reading use cases (news, docs), this is significant.
Cached assets load instantly. Less network dependency.
Users can add to home screen; the app feels more committed than a website.
Background sync, push notifications, file system access, camera, geolocation — increasingly available via web APIs.
Heavy graphics (games, video editing), tight integration with OS features. Native is still ahead for these.
Apple has historically been slow to adopt PWA features. Push notifications on iOS Safari only arrived recently. Some features still missing.
Most users find apps via App Store / Play Store. PWAs aren't there by default (though some stores accept them).
Native app developer experience for things like share extensions, widgets, watch apps.
PWAs work well for:
PWAs work less well for:
For many use cases, "make the website work great offline" is sufficient. For others, native or React Native is the answer.
Try cache; fall back to network.
caches.match(request)
.then(cached => cached || fetch(request))
For static assets that rarely change.
Try network; fall back to cache.
fetch(request).catch(() => caches.match(request))
For data that should be fresh; cache as fallback.
Serve cached; update cache in background.
caches.match(request).then(cached => {
fetch(request).then(fresh => cache.put(request, fresh));
return cached || fetch(request);
});
Best of both for most resources.
Serve from cache; never go to network. For truly static content.
Always go to network. For requests that must be fresh.
Service workers can be tricky to debug. They cache aggressively; old service workers persist. DevTools have specific service worker panels.
When a new service worker is published, the old one is still running for existing tabs. Until tabs close, users see old behavior. Strategies:
Browsers limit cache storage (varies by browser; tens to hundreds of MB). Plan caching strategy accordingly.
Service workers require HTTPS (or localhost). Production PWAs need TLS.
Google library for service worker patterns. Reduces boilerplate.
Next.js, Nuxt, SvelteKit — all have PWA plugins or built-in support.
Browser tool; audits PWA conformance. Useful for checking compliance.