How do you build a PWA in 2026?
Short answer: A production PWA in 2026 needs three things you cannot skip: a service worker that handles caching and offline, a Web App Manifest that makes the app installable, and HTTPS. Once those work, add the Push API when you want people to come back and a custom install prompt when you want more of them to install. And because iOS 16.4+ now handles web push, a single codebase finally reaches every platform — something that was not true even two years ago.
Treat this guide as the hands-on build playbook. It walks through service workers, caching, the manifest, offline mode, push notifications, and where iOS and Android still diverge in what they let you do. If you are still weighing the architecture itself, our web application development team ships PWAs as a first-class output, and the comparison piece Web App vs Native vs PWA weighs cost against reach before you commit.
What makes a PWA a PWA?
Google engineers Alex Russell and Frances Berriman coined the term Progressive Web App in 2015. A decade on, what started as a browser experiment has become a serious production strategy — Twitter (now X Lite), Pinterest, Starbucks and Uber all ship one. The word "progressive" points to the layered approach. At its core a PWA is just a web application; it then enhances its own behaviour wherever the browser happens to support the extra capability.
Strip it back and three technical pillars hold a PWA up:
- 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).
Past those three, the wider feature set adds the Push API, Background Sync, the Badging API, the File System Access API, Web Bluetooth, Web NFC and the Screen Wake Lock API. Support for each one varies from browser to browser, and that is exactly why the "progressive" framing earns its keep: the core experience runs everywhere, while the richer features switch on only where a given browser can back them.
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
No other piece of a PWA carries as much weight as the service worker. Learn it properly and you sidestep the bugs that give PWAs their bad name when teams rush the implementation: stale caches, updates that never land, offline fallbacks that fail silently.
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));
});
}
Wrapping the call in window.addEventListener('load', …) keeps the service worker install from fighting your page-critical downloads for bandwidth. Skip that step and you push out First Contentful Paint on the very first visit — the moment when a slow load hurts most.
Caching strategies in practice
Your fetch handler is where you decide how each resource gets served. Do not go looking for one best strategy, because there isn't one. What fits depends on two things: how often the resource changes, and how much staleness your users can live with.
Workbox, from Google, has become the default library for these patterns. It takes care of precaching with integrity hashes, runtime caching with expiration plugins, and keeping cache names straight across service worker updates. Unless you have a real reason to hand-write your own caching logic, reach for 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 tells the browser how your PWA should look and behave once it is installed. Chrome's Lighthouse reads it and flags any errors, and the browser itself uses it to build both the install prompt and the icon that lands on the home screen.
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 someone to install your PWA is a conversion, and you should treat it like one. The browser's own install UI is quiet enough that most users never notice it. Build your own prompt instead and install rates climb.
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).
Treat install rate as a product metric and A/B test when the prompt appears. Waiting until the user has done something that matters — saved a document, placed a first order — converts far better than firing the prompt the moment the page loads.
Building for offline mode
Nothing sets a PWA apart from an ordinary web app quite like offline support. Get it right and the app still does something useful — fully or in a pared-back form — when the network drops. Get it wrong and the user stares at a blank screen or a cryptic browser error, which is arguably worse than shipping no PWA at all.
Start with the app shell. Precache the handful of assets that draw the UI skeleton — navigation, layout, brand assets — and serve them straight from cache on every load. The dynamic content then drops in afterwards, either from the network or from a local cache. Google Maps, Twitter Lite and Starbucks all lean on this pattern.
Once the shell is in place, three APIs do the heavy lifting for offline work:
- 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?v=min20260719', '/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
Few re-engagement tools match a well-run push notification — and few get abused as often. Ask for permission at the wrong moment and users block your app on the spot, with no way back. Time the ask well and keep the content worth reading, and your engagement can hold its own against a native app.
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
iOS and Android have drawn much closer over the past three years, yet a few gaps still matter. The table below reflects where things stand in mid-2026, 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
Now that the capabilities and limits are on the table, here is how we actually decide at YuSMP Group, drawn from the kinds of products we build:
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 a lot of B2B SaaS products, e-commerce platforms and productivity tools, a PWA is simply the best return on the money in 2026. You build it once, it reaches everyone, and you push updates without waiting on an app store reviewer. The web application development service our team delivers treats PWAs as a first-class output, right alongside standard SPAs and server-rendered apps. For the full cost-and-reach breakdown, our comparison Web App vs Native vs PWA: How to Choose in 2026 goes deeper.
PWA build checklist
Run through this checklist before you ship. It follows the same criteria Lighthouse checks in its PWA audit, plus a handful of production-readiness items we picked up the hard way 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.