Anna Kowalski, YuSMP Group
Anna Kowalski Senior Mobile Engineer, YuSMP Group · Shipping React Native, Flutter and PWA products for US & EU clients since 2017

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:

  1. 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.
  2. Installation. The install event 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.
  3. Activation. After installation, the new service worker waits until all tabs using the old version are closed before activating. During the activate event you delete old caches. Calling self.skipWaiting() forces immediate activation (useful in development; be careful in production as it can break in-flight requests).
  4. Fetch interception. Once active, the service worker intercepts every network request from pages it controls via the fetch event. This is where caching strategies live.
  5. 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 change start_url, specifying id ensures 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.

Smartphone displaying a Progressive Web App installed on the home screen alongside native apps, illustrating PWA install UX on Android and iOS
When installed, a PWA appears on the home screen alongside native apps and opens in standalone mode — no browser chrome. The install flow on Android uses a system prompt; on iOS it requires the user to tap Share and then "Add to Home Screen".

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.sync for 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'))
    );
  }
});
Mobile app showing a cached offline content page while the device has no internet connection, demonstrating PWA offline mode
A well-designed offline page retains branding and communicates status clearly — "You are offline, but your recent data is saved." Never show the browser's default error screen to PWA users.

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:

  1. 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.
  2. Subscribe to push service. Call registration.pushManager.subscribe() with your VAPID public key. The browser returns a PushSubscription object containing the endpoint URL and encryption keys.
  3. Save subscription on your server. POST the PushSubscription JSON to your API and store it against the user account.
  4. 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.
  5. Show in service worker. The service worker receives a push event, parses the payload, and calls self.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:

CapabilityAndroid (Chrome 125+)iOS (Safari 17.x)
Add to home screen / install promptSystem prompt (beforeinstallprompt)Manual — Share > Add to Home Screen
Standalone display modeFull supportFull support
Service workers & cachingFull supportFull support
Web Push notificationsFull supportInstalled PWA only (iOS 16.4+)
Background SyncFull supportPartial (one-shot sync only, iOS 13+)
Background FetchSupported (Chrome 74+)Not supported
Persistent storage (navigator.storage.persist())Supported, user grant requiredSupported (iOS 15.2+)
Badging API (icon badge counts)Full supportSupported (iOS 16.4+)
Web Share APIFull supportFull support
Screen Wake LockFull supportNot supported
Web BluetoothSupported (Chrome, not Firefox)Not supported
Web NFCSupported (Chrome Android)Not supported
File System Access APIFull supportPartial (Safari 15.2+ read-only)
Media capture (camera/mic)Full supportSupported (iOS 14.3+)
Payment Request APIFull support (Google Pay integration)Supported (Apple Pay integration)
WebAuthn / PasskeysFull supportFull 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.

AreaChecklist itemNotes
HTTPSServed over HTTPS everywhereIncluding all subdomains used by the app
ManifestValid manifest linked in <head>Validate with Lighthouse or browser DevTools > Application
ManifestIcons at 192px and 512px (any + maskable)Missing maskable = white-padded icon on Android
ManifestScreenshots for richer install UIAt least one narrow (mobile) screenshot
Service workerRegistered and activeCheck DevTools > Application > Service Workers
Service workerfetch handler presentRequired for installability criteria
OfflineOffline fallback page returns 200Test with DevTools > Network > Offline mode
OfflineApp shell assets precachedNavigation + CSS + logo visible offline
InstallCustom install prompt on AndroidCapture beforeinstallprompt; don't rely on mini-infobar
InstalliOS install modal/tooltipDetect iOS + not standalone; show Share gesture guide
PerformanceLCP < 2.5s on 4G (Lighthouse)App shell from cache should be sub-1s on repeat visit
PerformanceINP < 200msInteraction to Next Paint replaced FID in CWV 2024
PushPermission requested on user gesture onlyNever on page load — instant block rate
Pushnotificationclick handler in service workerOpens app/page; closes notification
AccessibilityLighthouse accessibility score >= 90WCAG 2.2 AA — relevant for BITV (DE), EAA (EU)
SecurityContent-Security-Policy header setReport-only first; enforce after audit
UpdateCache versioned and cleared on activateOld caches deleted in activate event; test update path
UpdateUI 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.