What makes a PWA a PWA
The term Progressive Web App was coined by Google engineers Alex Russell and Frances Berriman in 2015. A decade later, the concept has matured from a browser experiment into a legitimate production strategy used by companies ranging from Twitter (now X Lite) and Pinterest to Starbucks and Uber. The word "progressive" refers to the layered approach: a PWA is a web application at its core, and it progressively enhances its own behaviour as the browser environment supports it.
Three technical pillars define a PWA:
- Service worker. A JavaScript file that runs in a separate thread, acts as a network proxy between the browser and the internet, and enables offline functionality, background sync and push notifications.
- Web App Manifest. A JSON file that tells the browser how to present the app when it is installed — its name, icons, theme colour, display mode and start URL.
- HTTPS. Service workers require a secure context. Every production PWA must be served over HTTPS (localhost is exempt during development).
Beyond these three, the broader PWA feature set includes the Push API, the Background Sync API, the Badging API, the File System Access API, Web Bluetooth, Web NFC, and the Screen Wake Lock API. Browser support varies — which is precisely why the "progressive" framing matters. Your core experience works everywhere; enhanced features engage where they are available.
If you are still deciding whether a PWA is the right architecture for your product, our article Web App vs Native vs PWA: How to Choose in 2026 walks through the full cost, reach and capability trade-off. This guide assumes you have made the call and want to build.
Service workers: the engine under the hood
A service worker is the single most consequential piece of a PWA. Understanding it deeply prevents the most common production bugs — stale caches, update delays, broken offline fallbacks — that give PWAs a bad reputation when they are implemented carelessly.
Here is how the lifecycle works:
- Registration. The main page registers the service worker script:
navigator.serviceWorker.register('/sw.js'). The browser downloads and parses the script in a separate thread. - Installation. The
installevent fires. This is where you open a cache and precache your app shell — the minimal set of HTML, CSS and JS needed to render the UI without a network. - Activation. After installation, the new service worker waits until all tabs using the old version are closed before activating. During the
activateevent you delete old caches. Callingself.skipWaiting()forces immediate activation (useful in development; be careful in production as it can break in-flight requests). - Fetch interception. Once active, the service worker intercepts every network request from pages it controls via the
fetchevent. This is where caching strategies live. - Idle and termination. The browser can terminate an idle service worker at any time to conserve memory. It is re-spawned on demand. Never store state in global service worker variables — use IndexedDB or the Cache API instead.
A common mistake is registering the service worker too early, before critical assets have loaded. The recommended pattern:
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('SW registered:', reg.scope))
.catch(err => console.error('SW registration failed:', err));
});
}
Registering inside window.addEventListener('load', …) ensures the service worker installation does not compete with page-critical resource downloads, which would delay First Contentful Paint on first visit.
Caching strategies in practice
The service worker's fetch handler is where you decide how each type of resource is served. There is no single best strategy — the right approach depends on how often the resource changes and how much staleness is acceptable.
Workbox (from Google) is the de-facto standard library for implementing these patterns. It handles precaching with integrity hashes, runtime caching with expiration plugins, and cache-name management across service worker updates. Unless you have a compelling reason to hand-write caching logic, use Workbox.
The four patterns you will use in almost every PWA:
- Cache-First. Check the cache first; only hit the network if the resource is not cached. Ideal for static assets (fonts, icons, versioned JS/CSS bundles) that change infrequently. Delivers the fastest possible load time.
- Network-First. Try the network; fall back to cache on failure. Best for API endpoints where data freshness is critical (e.g., a user's account balance or an order status). The cache acts as an emergency fallback when the user is offline.
- Stale-While-Revalidate. Serve from cache immediately, then update the cache in the background from the network. Balances speed and freshness for resources that update periodically — article feeds, product listings, non-critical API responses. The user always gets a fast response; the next load gets fresh data.
- Network-Only. Always go to the network; never use the cache. Use for analytics pings, payments, login/logout, or any request where stale data is never acceptable.
A Workbox configuration that covers a typical SPA or Next.js app:
// sw.js (with Workbox imported via CDN or bundler)
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
// Precache app shell (injected at build time by workbox-webpack-plugin)
precacheAndRoute(self.__WB_MANIFEST);
// Static assets — Cache-First, 30-day expiry
registerRoute(
({ request }) => request.destination === 'image' || request.destination === 'font',
new CacheFirst({ cacheName: 'static-assets', plugins: [new ExpirationPlugin({ maxAgeSeconds: 30 * 24 * 60 * 60 })] })
);
// API responses — Network-First, 7-day fallback
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new NetworkFirst({ cacheName: 'api-cache', plugins: [new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 7 * 24 * 60 * 60 })] })
);
// Page navigations — Stale-While-Revalidate
registerRoute(
({ request }) => request.mode === 'navigate',
new StaleWhileRevalidate({ cacheName: 'pages' })
);
Web app manifest and installability
The Web App Manifest is a JSON file that defines how your PWA looks and behaves when installed. Chrome's Lighthouse audits it and reports errors; the browser uses it to generate the install prompt and the installed app icon.
A production-ready manifest for 2026:
{
"name": "My PWA App",
"short_name": "MyPWA",
"description": "A short description visible in some OS app stores",
"start_url": "/?utm_source=pwa",
"id": "/",
"display": "standalone",
"background_color": "#0B1020",
"theme_color": "#2b8ad6",
"orientation": "any",
"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-512-maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
],
"screenshots": [
{ "src": "/screenshots/desktop.png", "sizes": "1280x800", "type": "image/png", "form_factor": "wide" },
{ "src": "/screenshots/mobile.png", "sizes": "390x844", "type": "image/png", "form_factor": "narrow" }
],
"categories": ["productivity", "utilities"],
"shortcuts": [
{ "name": "Dashboard", "url": "/dashboard", "icons": [{ "src": "/icons/shortcut-dashboard.png", "sizes": "96x96" }] }
]
}
Link it from your HTML <head>:
<link rel="manifest" href="/manifest.json">
Key manifest fields to get right in 2026:
id(new in 2023). A stable identifier for the app. If you changestart_url, specifyingidensures the browser treats the new version as an update to the existing installed app rather than a new app.screenshots. Required for the "richer install UI" that Android 14 + Chrome 119+ shows before the install prompt — a preview of the app. Google Play-style cards appear automatically when screenshots are present.- Maskable icons. Android adaptive icons crop to a circle, squircle or other shape. Without a maskable icon your logo will appear with white padding inside the shape. Use maskable.app to generate and preview.
display: "standalone". The installed app hides the browser chrome (address bar) and looks like a native app."minimal-ui"keeps a minimal back/reload bar — useful if your app relies on browser navigation.
Install prompts and add-to-home-screen
Getting users to install your PWA is a conversion event just like any other. The default browser install UI is easy to miss. Building a custom install prompt dramatically improves install rates.
On Android (Chrome): the browser fires the beforeinstallprompt event when all installability criteria are met. Capture it and trigger it on a user gesture:
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault(); // Suppress the automatic mini-infobar
deferredPrompt = e;
showInstallButton(); // Reveal your custom "Install App" button
});
installButton.addEventListener('click', async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log('User choice:', outcome); // 'accepted' or 'dismissed'
deferredPrompt = null;
hideInstallButton();
});
window.addEventListener('appinstalled', () => {
console.log('PWA installed');
analytics.track('pwa_installed');
});
On iOS (Safari): there is no beforeinstallprompt event. The user must manually tap the Share button and then "Add to Home Screen". Your best option is a modal that appears after the user has visited the app a few times, explaining the gesture with a short animation. Detect iOS with navigator.userAgent and check whether the app is already installed with window.navigator.standalone (returns true on iOS when running as a home-screen app).
Track install rate as a product metric. A/B test the timing of the install prompt — showing it after the user has completed a meaningful action (e.g., saved a document or placed a first order) converts significantly better than showing it on first load.
Building for offline mode
Offline support is the feature that most dramatically differentiates a PWA from a standard web app. Done well, it delivers a full or degraded-but-useful experience when the user has no network. Done poorly, it shows a blank screen or a cryptic browser error — worse than no PWA at all.
The foundational pattern is the app shell: precache the minimal set of assets needed to render the UI skeleton (navigation, layout, brand assets) and serve them from cache on every load. Dynamic content then fills in from the network or from a local cache. Google Maps, Twitter Lite and Starbucks all use this pattern.
Beyond the app shell, three APIs power offline functionality:
- Cache API. Key-value store for Request/Response pairs. Lives inside service workers. Workbox wraps it with expiration, cleanup and versioning. Use it for pages, API snapshots and media.
- IndexedDB. A full client-side database with transactions, indexes and cursors. Use it for structured data that needs querying — user-created content, drafts, offline queue items. Libraries like Dexie.js make the API ergonomic.
- Background Sync API. Queues network requests made while offline and replays them when connectivity returns. The canonical use case: a user submits a form while on the underground; Background Sync delivers it silently once they have signal again. Check
ServiceWorkerRegistration.syncfor browser support before relying on it.
Always provide an explicit offline fallback page. Register it in your service worker's install event and return it in the fetch handler when navigation fails:
// In sw.js install event, precache the offline page
self.addEventListener('install', (e) => {
e.waitUntil(
caches.open('app-shell-v2').then(cache =>
cache.addAll(['/offline.html', '/assets/css/main.css', '/assets/img/logo.svg'])
)
);
});
// In fetch handler, return offline page for failed navigations
self.addEventListener('fetch', (e) => {
if (e.request.mode === 'navigate') {
e.respondWith(
fetch(e.request).catch(() => caches.match('/offline.html'))
);
}
});
Push notifications end-to-end
Push notifications are one of the most powerful re-engagement tools available to a PWA — and one of the most frequently abused. Get the permission request wrong and users will block your app immediately. Get the timing and content right and you can achieve engagement rates that rival native apps.
The technical flow has five steps:
- Request permission. Call
Notification.requestPermission()inside a user gesture (button click, not on page load). This shows the browser's permission dialog. If the user denies, you cannot ask again — browser permission is sticky. - Subscribe to push service. Call
registration.pushManager.subscribe()with your VAPID public key. The browser returns aPushSubscriptionobject containing the endpoint URL and encryption keys. - Save subscription on your server. POST the
PushSubscriptionJSON to your API and store it against the user account. - Send from server. Use the Web Push protocol (or a library like web-push for Node.js) to POST an encrypted payload to the endpoint. Firebase Cloud Messaging (FCM) can act as the push delivery service for both Android and iOS.
- Show in service worker. The service worker receives a
pushevent, parses the payload, and callsself.registration.showNotification(title, options)to display the notification.
// sw.js — handle incoming push
self.addEventListener('push', (e) => {
const data = e.data?.json() ?? { title: 'New message', body: '' };
e.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icons/icon-192.png',
badge: '/icons/badge-72.png',
data: { url: data.url ?? '/' },
actions: [
{ action: 'open', title: 'Open' },
{ action: 'dismiss', title: 'Dismiss' }
]
})
);
});
// Handle notification click
self.addEventListener('notificationclick', (e) => {
e.notification.close();
if (e.action === 'dismiss') return;
e.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true }).then(clientList => {
const target = e.notification.data?.url ?? '/';
for (const client of clientList) {
if (client.url === target && 'focus' in client) return client.focus();
}
if (clients.openWindow) return clients.openWindow(target);
})
);
});
iOS note: Push notifications for home-screen PWAs are supported from iOS 16.4 (Safari 16.4 / March 2023). The PWA must be installed to the home screen — push does not work for PWAs running in the browser tab on iOS. Prompt users to install first, then request notification permission.
PWA capabilities: iOS vs Android in 2026
Platform parity between iOS and Android has improved substantially over the past three years, but meaningful gaps remain. This table is accurate as of mid-2026 based on WebKit 17.x and Chrome 125:
| Capability | Android (Chrome 125+) | iOS (Safari 17.x) |
|---|---|---|
| Add to home screen / install prompt | System prompt (beforeinstallprompt) | Manual — Share > Add to Home Screen |
| Standalone display mode | Full support | Full support |
| Service workers & caching | Full support | Full support |
| Web Push notifications | Full support | Installed PWA only (iOS 16.4+) |
| Background Sync | Full support | Partial (one-shot sync only, iOS 13+) |
| Background Fetch | Supported (Chrome 74+) | Not supported |
Persistent storage (navigator.storage.persist()) | Supported, user grant required | Supported (iOS 15.2+) |
| Badging API (icon badge counts) | Full support | Supported (iOS 16.4+) |
| Web Share API | Full support | Full support |
| Screen Wake Lock | Full support | Not supported |
| Web Bluetooth | Supported (Chrome, not Firefox) | Not supported |
| Web NFC | Supported (Chrome Android) | Not supported |
| File System Access API | Full support | Partial (Safari 15.2+ read-only) |
| Media capture (camera/mic) | Full support | Supported (iOS 14.3+) |
| Payment Request API | Full support (Google Pay integration) | Supported (Apple Pay integration) |
| WebAuthn / Passkeys | Full support | Full support (iOS 16+) |
When a PWA is the right call
With a clear picture of what PWAs can and cannot do in 2026, here is a practical decision framework based on the product types we build at YuSMP Group:
Build a PWA when:
- Your primary distribution channel is web search (SEO) and you do not want to fragment discoverability across an app store and a web page.
- Your users are on a diverse set of devices and operating systems — B2B SaaS users on corporate Windows laptops, field workers on Android, and executives on iPhones all hit the same URL.
- Your offline requirement is moderate — cached content, queued form submissions, basic CRUD from a local IndexedDB. You do not need large background downloads or complex hardware access.
- Budget constraints make maintaining separate iOS and Android native codebases impractical, but the React Native or Flutter cross-platform option still feels heavyweight for your feature set.
- You are building a content-heavy product (news, documentation, courses, e-commerce catalogue) where app-store review latency would slow content updates.
Consider native or React Native instead when:
- You need Bluetooth, NFC, continuous background GPS, or advanced AR capabilities.
- Your product is a game or an audio/video streaming experience where you need codec access, background audio or low-level rendering APIs.
- Your iOS users are a significant segment and they need push notifications delivered to the browser tab (not just when the PWA is installed).
- App store presence is a trust signal in your market — enterprise buyers in regulated industries often expect a listing in Apple Business Manager or Google Play.
- You need deep OS integration: widgets, Siri Shortcuts, home-screen actions, share extensions, watch app companion.
For many B2B SaaS products, e-commerce platforms and productivity tools, a PWA is the highest-ROI choice in 2026. You ship once, reach everyone, and iterate without app store review gates. The web application development service our team delivers covers PWAs as a first-class output alongside standard SPAs and server-rendered apps. You can also explore our detailed comparison in Web App vs Native vs PWA: How to Choose in 2026 for the full cost and reach analysis.
PWA build checklist
Use this checklist before shipping a PWA to production. It mirrors the criteria Lighthouse uses for its PWA audit, extended with production readiness items we have learned from shipping PWAs for US and EU clients.
| Area | Checklist item | Notes |
|---|---|---|
| HTTPS | Served over HTTPS everywhere | Including all subdomains used by the app |
| Manifest | Valid manifest linked in <head> | Validate with Lighthouse or browser DevTools > Application |
| Manifest | Icons at 192px and 512px (any + maskable) | Missing maskable = white-padded icon on Android |
| Manifest | Screenshots for richer install UI | At least one narrow (mobile) screenshot |
| Service worker | Registered and active | Check DevTools > Application > Service Workers |
| Service worker | fetch handler present | Required for installability criteria |
| Offline | Offline fallback page returns 200 | Test with DevTools > Network > Offline mode |
| Offline | App shell assets precached | Navigation + CSS + logo visible offline |
| Install | Custom install prompt on Android | Capture beforeinstallprompt; don't rely on mini-infobar |
| Install | iOS install modal/tooltip | Detect iOS + not standalone; show Share gesture guide |
| Performance | LCP < 2.5s on 4G (Lighthouse) | App shell from cache should be sub-1s on repeat visit |
| Performance | INP < 200ms | Interaction to Next Paint replaced FID in CWV 2024 |
| Push | Permission requested on user gesture only | Never on page load — instant block rate |
| Push | notificationclick handler in service worker | Opens app/page; closes notification |
| Accessibility | Lighthouse accessibility score >= 90 | WCAG 2.2 AA — relevant for BITV (DE), EAA (EU) |
| Security | Content-Security-Policy header set | Report-only first; enforce after audit |
| Update | Cache versioned and cleared on activate | Old caches deleted in activate event; test update path |
| Update | UI prompt to refresh on new version | "New version available — tap to reload" toast improves UX |
Run npx lighthouse https://your-pwa.com --view before any production release. The PWA section of the Lighthouse report maps directly to this checklist and flags regressions automatically.
For teams new to PWA development, our web application development service includes PWA audits and migrations as a standalone engagement — we assess your existing web app and deliver a prioritised roadmap to make it installable, offline-capable and push-enabled.
FAQ
What is a Progressive Web App and how is it different from a regular web app?
A Progressive Web App is a web application that uses modern browser APIs — primarily service workers, the Web App Manifest and the Push API — to deliver an experience comparable to a native app. Unlike a regular web app it can work offline, cache assets for near-instant loads, receive push notifications and be installed to the home screen without going through an app store. The codebase is still HTML, CSS and JavaScript; the difference is a layer of capabilities layered on top.
Do PWAs work on iOS in 2026?
Yes, with caveats. iOS 16.4+ supports Web Push (including home-screen PWAs), Background Sync, the Badging API and persistent storage requests. However, iOS still lacks Background Fetch, full Bluetooth and NFC access, Screen Wake Lock and the full File System Access API. The install flow on iOS requires the user to manually tap "Add to Home Screen" in Safari — there is no system-level install prompt. For most content-driven, commerce and productivity PWAs, iOS support is sufficient in 2026.
What caching strategy should I use for my PWA?
It depends on the resource type. Use Cache-First for static assets (JS, CSS, fonts, logo) that rarely change — serve from cache, update in background. Use Network-First for API calls where freshness matters — try the network, fall back to cache on failure. Use Stale-While-Revalidate for pages where slightly stale content is acceptable but you want fast loads. Workbox from Google automates all three patterns with precaching, runtime caching and cache expiration built in.
How do I trigger the 'Add to Home Screen' install prompt on Android?
Android Chrome fires the beforeinstallprompt event when the PWA meets the installability criteria: HTTPS, a valid Web App Manifest with name, short_name, icons (at least 192x192 and 512x512), start_url, and display set to standalone or fullscreen, plus a registered service worker with a fetch handler. Capture the event with window.addEventListener('beforeinstallprompt', …), store it, and call prompt() on it when the user clicks your custom install button. On iOS, the prompt must be triggered manually — present a modal explaining the Share > Add to Home Screen gesture.
Can a PWA send push notifications?
Yes. The Push API combined with the Notifications API lets you send push notifications via a server-side push service. The flow is: request notification permission in the browser, subscribe to a push service to get a PushSubscription object, send that subscription to your server, POST a push message from your server to the push endpoint, and the service worker receives a push event and shows a notification. On iOS this requires the PWA to be installed to the home screen (iOS 16.4+).
When should I build a PWA instead of a native app?
A PWA is the right call when your primary goal is web discoverability and SEO, you need to reach users across devices without app store friction, your budget does not support separate iOS and Android codebases, your offline requirements are moderate, and you do not need Bluetooth, NFC, AR, or deep OS integration. If you need any of those hardware APIs, or your product is a game or media-heavy experience, native or React Native is the better choice. See our full comparison at Web App vs Native vs PWA: How to Choose in 2026.
How large can a PWA's cache be?
Storage quotas depend on the browser and available disk space. Chrome grants roughly 60% of total disk space for all origin storage. In practice, a PWA on a phone with 64 GB can use several GB. However, the browser can evict storage under pressure unless you call navigator.storage.persist() and the user grants persistent storage. Budget your cache thoughtfully: precache only shell assets (usually under 5 MB) and cache API responses selectively with expiration limits.
Last updated 16 June 2026. Technical details reflect WebKit 17.x and Chrome 125+ support matrices current as of mid-2026. Browser capabilities evolve rapidly — verify against MDN and caniuse.com before finalising implementation decisions.