Progressive web apps
A website that the browser can install as an app: a manifest gives it a name, icons, and its own window, a service worker makes it open without the network, and the result lives on the home screen without an app store.
The problem PWAs solve
Picture a small chain of gyms. Members book classes on the gym's website from their phones. The site works well, but members keep asking for "the app". They want an icon on the home screen, a full-screen view without the address bar, and a booking page that opens even in the basement changing room, where the signal drops to nothing.
The gym looks at a native app. It would need an iOS team and an Android team (or a cross-platform framework), two store reviews for every release, and a download of 40 to 80 MB that many members will never finish. The website already does everything the app would do. What it lacks is a home: a place on the device, a window of its own, and a way to start without the network.
A progressive web app (PWA) is a website that fills exactly that gap. It is the same HTML, CSS, and JavaScript, served from the same URL. With two small additions, a web app manifest and a service worker, the browser lets the user install it. After that it has an icon, opens in its own window, and can start offline.
What changes for the user
The code is the same. What changes is how the app is reached and how it looks:
- An icon on the home screen, in the dock, or in the Start menu, next to native apps. No URL to remember.
- Its own window, without the address bar and tabs, shown as a separate app in the task switcher.
- A fast, reliable start. A service worker serves the app's shell from a cache, so the first paint does not wait for the network. In the basement it still opens and shows the last known bookings.
- Some app powers. Depending on the platform, it can show notifications, put a badge on its icon, receive shared files, or open files of a type it handles.
Why "progressive"
The word means the app gets better as the platform gets more capable, and never worse. A browser that cannot install apps still gets a working website; one that can gets an installable app; one with push or file handling gets those too. Every feature is checked before it is used, so no user gets a broken page.
A native app either runs on a platform or does not. A PWA runs everywhere and gets as much as each platform allows, which differs a lot between Chrome on Android and Safari on iOS. A big part of this lesson is about those differences.
What a PWA is not
A PWA is not a framework or a special build. A plain multi-page site with server-rendered HTML can be a PWA, and a single-page React app can fail to be one. It is also not "offline mode" alone: offline comes from the service worker and install from the manifest, and the next chapter keeps the two apart.
This lesson is the overview. The service worker itself (lifecycle, caching strategies, updates), push notifications, and client storage each have their own lesson, which we link to rather than repeat.
Check your understanding · Exercise 1
Which of these are true of a progressive web app? Select all that apply.
The mental model: a website with three additions
Picture an ordinary website with three additions on top, each unlocking one part of the app experience. Take any of them away and you lose that part, but the website still works.
- HTTPS. Installing an app and running a service worker are powerful, so browsers only allow them on secure origins.
http://localhostcounts as secure for development. - A web app manifest. A small JSON file that gives the app its identity: name, icons, colors, the URL to open, and how the window looks. It is what makes the site installable.
- A service worker. A script that sits between the page and the network and can answer requests from a cache. It is what makes the app reliable: it opens fast and works offline.
The manifest and the service worker are often confused, so separate them firmly. The manifest is passive data: the browser reads it, the page never runs it. The service worker is active code: it runs in the background, handles fetch events, and may receive push events. A site can have either one without the other.
What "install" actually creates
Installing does not copy the website onto the device the way an app store does. It creates an entry point: an icon that opens a URL inside a window with no browser chrome (chrome means the address bar, tabs, and buttons around a page). What exactly the browser creates depends on the platform:
| Platform | What install creates | Where the user finds it |
|---|---|---|
| Chrome on Android | A WebAPK: a small real Android package, generated and signed by a Google server from your manifest | Home screen and app drawer, app settings, and the share sheet if it declares share_target |
| Chrome or Edge on desktop | An OS app entry that launches the browser in an app window | Start menu, Dock or Launchpad, taskbar, the browser's apps page |
| Safari on iOS and iPadOS | A Home Screen web app that runs in its own web view, with its own storage | Home Screen and App Library |
| Safari on macOS 14+ | A web app in the Dock, created with Add to Dock | Dock, Applications folder, Spotlight |
In every case the code still comes from your origin, and the service worker cache is what makes it available offline. Uninstalling removes the entry point and, on some platforms, offers to clear the site's data.
One origin, one app (mostly)
Browsers tie an installed app to an origin (scheme, host, and port, such as https://gym.example), and its storage, cookies, permissions, and service worker belong to that origin. Two apps on one origin would share all of that, so the usual advice is one app per origin, such as app.gym.example and admin.gym.example.
Check your understanding · Exercise 2
A site has a valid manifest and HTTPS, but no service worker. What happens in Chrome on Android? Select all that apply.
The web app manifest, field by field
The manifest is a JSON file, usually named manifest.webmanifest or manifest.json, linked from the head of every page with <link rel="manifest" href="/manifest.webmanifest">. Link it from every page, not only the home page: the user may decide to install from any page, and the browser only knows about the manifest of the page it is showing.
Here is a complete manifest for the gym app. Every field is explained below, grouped by what it controls.
{
"id": "/app",
"name": "Northside Gym: classes and bookings",
"short_name": "Northside",
"description": "Book classes, see your passes, check in at the door.",
"start_url": "/app?source=pwa",
"scope": "/app",
"display": "standalone",
"display_override": ["window-controls-overlay", "standalone"],
"theme_color": "#1f4e79",
"background_color": "#ffffff",
"icons": [
{ "src": "/icons/192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/512.png", "sizes": "512x512", "type": "image/png" },
{ "src": "/icons/maskable-512.png", "sizes": "512x512",
"type": "image/png", "purpose": "maskable" }
],
"shortcuts": [
{ "name": "Book a class", "url": "/app/book",
"icons": [{ "src": "/icons/book-96.png", "sizes": "96x96" }] }
],
"screenshots": [
{ "src": "/shots/phone.png", "sizes": "1080x1920",
"type": "image/png", "form_factor": "narrow" }
]
}
application/manifest+json content type. Browsers are lenient about the type, but some audit tools are not.id must never change; the rest can be tuned later, but reach installed apps slowly.Identity: name, short_name, description, id
nameis the full name, shown in the install dialog, the splash screen, and the OS app list.short_nameis shown under the home screen icon, where about 12 characters fit before the OS cuts the text. If you only givename, a long name gets cut in an ugly place.descriptionappears in Chrome's richer install dialog and in store listings created from the manifest.idis the app's permanent identity. If it is missing, browsers usestart_urlas the identity. That is a trap: changestart_urllater (say, to add a tracking parameter) and the browser sees a different app, so users can end up with two icons. Setidon day one and never change it.
Launch: start_url and scope
start_url is the URL the icon opens. A query such as ?source=pwa lets analytics count launches from the installed app. It must be inside scope, and it should be a page the service worker can serve offline, or the installed app shows an error every time it starts in a tunnel.
scope is the set of URLs that belong to the app: every URL whose path starts with the scope. If you leave it out, the scope is the folder that contains start_url. When the user follows a link outside the scope, the browser shows its own toolbar with the URL or opens a browser view, so the user can see they have left the app. Sign-in pages are the classic victim; the failure modes chapter shows the fix.
Window: display and display_override
display value | What the user sees | Good for |
|---|---|---|
fullscreen | The whole screen, no status bar, no browser UI | Games, kiosks, video |
standalone | Its own window with the OS status bar, no browser UI | Most apps |
minimal-ui | Its own window with a small bar: back, reload, URL | Content apps where back matters |
browser | A normal browser tab (this means: not an app window) | Sites that should stay sites |
display_override is a list the browser tries in order before falling back to display. It exists for newer modes that older browsers do not know. For example, window-controls-overlay lets a desktop app draw into the title bar area, and is only supported in Chromium browsers on desktop. Safari ignores display_override and does not support minimal-ui.
Look: icons, colors, screenshots, shortcuts
icons: a list of images withsizes,type, and an optionalpurpose(any,maskable, ormonochrome). Chapter 6 covers them in depth.theme_color: the color of the title bar or status bar. A<meta name="theme-color">tag on the page overrides it, and can differ for light and dark mode.background_color: the splash screen background while the app starts. Match the background of your first paint, or the user sees a flash of the wrong color.screenshots: images that Chrome shows in a richer install dialog, like a store listing. Useform_factor: "narrow"for phones and"wide"for desktop.shortcuts: up to a handful of deep links that appear on a long-press of the icon (Android) or a right-click of the dock or taskbar icon (desktop Chromium, and Safari 17.4 or later for Dock apps on macOS). Safari on iOS ignores them.
Check your understanding · Exercise 3
A team ships a manifest without id and start_url: "/". Six months later they change start_url to /?source=pwa. What happens? Select all that apply.
Installability and install prompts
Each browser has its own install criteria: a checklist a site must pass before an Install button or menu item appears. The lists differ, and Safari has none at all.
Chrome, Edge, and other Chromium browsers
Chrome is the strictest and the best documented, and Edge, Samsung Internet, and Opera follow it closely. Before Chrome shows its install promotion and fires beforeinstallprompt, all of these must be true:
- The page is served over HTTPS (or from
localhost). - The app is not already installed.
- The manifest has
nameorshort_name, astart_url, icons of at least 192 and 512 pixels, and adisplayoffullscreen,standalone,minimal-ui, orwindow-controls-overlay. prefer_related_applicationsis missing orfalse. If it istrue, Chrome points the user to your native app in the store instead.- The user has engaged: clicked or tapped the page at least once, and spent at least 30 seconds on it, at any time, even on an earlier visit.
Notice what is missing: a service worker. Chrome used to require one with a fetch handler, and many articles still say so, but it dropped that rule in 2023. You still want a service worker, because an installed app that shows the dinosaur offline page feels broken, but it is no longer the gate.
Safari on iOS, iPadOS, and macOS
Safari has no install criteria and no install prompt. Any site can be added, with or without a manifest; if there is one, Safari reads the name, start_url, display, and icons from it, and otherwise uses the page title and the apple-touch-icon link. Your job on Apple devices is to teach users the steps, because nothing in the browser will suggest it.
- iPhone and iPad: the user opens the Share menu and picks Add to Home Screen. Since iOS 16.4, Chrome, Edge, and Firefox on iOS can do the same from their share menus.
- What opens: before iOS 26, the icon opened a web app only if the manifest's
displaywasstandaloneorfullscreen, and Safari otherwise. Since iOS 26, every site added this way opens as a web app by default, and the user can turn off an Open as Web App switch. - Mac: since Safari 17 on macOS Sonoma, the user picks File, then Add to Dock. That always creates a web app, and Safari copies the site's cookies into it, so the user stays signed in.
Firefox
Firefox on Android can install sites to the home screen from its menu, using the manifest. On desktop, Firefox had no install support for years; in 2025, Firefox 143 on Windows added a way to pin a site to the taskbar as a simple web app. It fires no beforeinstallprompt, so plan for most desktop Firefox users as tab users.
{
"name": "Northside Gym",
"short_name": "Northside",
"display": "browser",
"icons": [
{ "src": "/icons/192.png", "sizes": "192x192", "type": "image/png" }
]
}Chrome, Edge (Android and desktop): not installable.
- Missing: A 512 by 512 icon
- Missing:
start_url - Missing:
displayis an app mode (nowbrowser) - No service worker: it can install, but opens to an offline error without a network.
- No
id: the app's identity followsstart_url, so changing it later can create a second app. - No maskable icon: Android shrinks the icon onto a white circle.
- No
screenshots: Chrome shows the basic install dialog, not the richer one.
Safari on iPhone and iPad: app on iOS 26+.
- No prompt: the user picks Share, then Add to Home Screen. On iOS 26 and later it still opens as a web app unless the user turns off Open as Web App; older versions open it in Safari, like a bookmark.
- App icon: the manifest icons.
- Opens to an error offline, since nothing is cached.
- Push works only after it is added to the Home Screen (iOS 16.4+).
Safari on macOS: Add to Dock.
- No prompt: the user picks File, then Add to Dock. Any site can be added; the manifest is optional.
- App icon: the manifest icons.
- Opens to an error offline, since nothing is cached.
Showing your own install button
When a Chromium browser decides the site qualifies, it fires beforeinstallprompt on window. If you call preventDefault() and keep the event, the browser does not show its own mini-infobar on Android, and you can call prompt() later from your own button. Each saved event can be used for one prompt.
let saved: BeforeInstallPromptEvent | null = null;
window.addEventListener("beforeinstallprompt", (event) => {
event.preventDefault();
saved = event as BeforeInstallPromptEvent;
showInstallButton();
});
async function onInstallClick() {
if (!saved) return;
await saved.prompt();
const { outcome } = await saved.userChoice; // "accepted" | "dismissed"
analytics.track("install_prompt", { outcome });
saved = null;
hideInstallButton();
}
window.addEventListener("appinstalled", () => analytics.track("installed"));
const isInstalled =
matchMedia("(display-mode: standalone)").matches ||
(navigator as { standalone?: boolean }).standalone === true; // iOS
BeforeInstallPromptEvent is not in TypeScript's DOM types, so declare it yourself. navigator.standalone is Safari's own flag for Home Screen apps.beforeinstallprompt.Decision When and how should the app ask users to install?
What we pick: Use the contextual button. Pick a moment of value (a finished booking, a third visit), keep a quiet permanent entry in the settings or account menu, and hide all install UI when display-mode: standalone matches.
Check your understanding · Exercise 4
Your manifest is valid and the site is on HTTPS, but beforeinstallprompt never fires during your quick test in Chrome. What could explain it? Select all that apply.
The app shell: opening like an app
Native apps never show a blank white screen while they download their own menu bar. The frame of the app is on the device, so it appears at once, and only the content waits for the network. The app shell pattern gives a PWA the same behavior.
The shell is the smallest set of HTML, CSS, and JavaScript that draws the app's frame: the header, the navigation, the layout of the main area, and empty placeholders. The service worker stores it in a cache when it installs. On every launch, the service worker answers the navigation request with the cached shell, and the shell then fetches the content.
A launch, with and without a shell
Take the gym app on a weak mobile connection with a 600 ms round trip. Without a shell, a launch is a normal page load: DNS, a TLS connection, the HTML, then the CSS and JavaScript it names. The user stares at the splash screen, then a white page, for 2 to 3 seconds, and offline the launch fails.
With a shell, the service worker answers the HTML, CSS, and JavaScript from its cache in tens of milliseconds. The frame is on screen in about 300 ms, mostly the time to parse and run the scripts. The bookings area shows the last saved bookings from IndexedDB, marked as "updated 2 hours ago", while a request for fresh data runs. Offline, everything except the fresh data still works.
Keeping the shell small
The shell is downloaded again after every deploy that changes it, and parsed on every launch. A useful budget for a mobile shell is about 100 to 150 KB of compressed JavaScript and CSS. Code for screens the user may never open belongs in lazy-loaded chunks, cached at runtime.
- In the shell: the layout, the router, the design system's core styles, the icons in the frame, an offline page, and the code that reads cached data.
- Not in the shell: the content itself, rarely used screens, large libraries such as a chart or rich-text editor, and user-specific HTML.
- Skeletons: placeholders with the same size as the real content, so nothing jumps when the data arrives. A skeleton that is the wrong size trades a blank screen for layout shift.
App shell or server-rendered pages?
The shell fits apps with a stable frame and changing data: mail, chat, dashboards, booking tools. For a content site, it can make the first view slower, because the article now waits for JavaScript to fetch it.
Decision How should an installed app answer a navigation (a launch or a page load)?
What we pick: For an app people use daily (bookings, mail, dashboards), use the app shell with cached data. For a content site, cache pages network first with an offline page. Combine them only when you have measured that both the launch and the content paint matter.
Check your understanding · Exercise 5
Which of these belong in the precached app shell of a booking app? Select all that apply.
Icons and splash screens
The icon is the first thing users see of an installed app, and on many devices the browser has to reshape it. The splash screen is the second thing: what fills the screen while the app starts. Both are generated from a few manifest fields, and both look broken if those fields are careless.
Sizes and formats
Give at least a 192 by 192 and a 512 by 512 PNG. The browser scales them to every place the icon appears: the home screen, the app switcher, the task bar, the install dialog, the splash screen. Chrome requires both sizes for installability.
Safari on iOS reads manifest icons in recent versions, but an apple-touch-icon link still wins when present. Give it a 180 by 180 PNG with no transparency: iOS fills transparent pixels with black, and it rounds the corners itself, so do not round them in the image.
Maskable icons
Android launchers show every icon in one shape, chosen by the device maker: a circle, a squircle (a rounded square with soft corners), a rounded square, or a teardrop. A plain square icon does not fit any of them. Without help, Android shrinks it and puts it on a white circle, which looks like a sticker on a plate.
A maskable icon solves this. It is a full-bleed square, colored to the edges, with everything important inside the safe zone: a central circle whose diameter is 80% of the icon, about 410 pixels across on a 512-pixel icon. Every launcher mask keeps the whole safe zone, so the logo is never cut. Mark it with "purpose": "maskable".
With no maskable icon, Android shrinks the square icon onto a white circle. It works, but looks like a sticker on a plate.
- Ship a separate
anyicon andmaskableicon. A single icon marked"any maskable"either has too much padding where it is shown unmasked, or gets clipped where it is masked. - Fill the whole square with a background color or pattern. Transparent corners in a maskable icon show up as black or white wedges after masking.
- Check the result in a maskable preview tool or in Chrome DevTools (Application, then Manifest).
monochromeicons are single-color shapes a platform can tint to match a themed home screen. Support is limited; treat them as optional.
Splash screens
On Android, Chrome builds the splash screen from the manifest. It fills the screen with background_color, puts one of your icons in the center, and on some versions writes name under it. Nothing to design, but three things to check: a 512 icon so the center icon is sharp, a background_color that matches the first frame of your app, and a name that is not cut off.
background_color, an icon, and the name. Match the color to your first paint, or the user sees a flash when the app takes over.Safari on iOS has not built splash screens from the manifest the way Android does. The reliable way is apple-touch-startup-image link tags: one full-screen image per device screen size and orientation, chosen with a media query. That is dozens of images to cover current iPhones and iPads, so most teams generate them with a build tool, or accept a plain screen in the background color.
<link rel="apple-touch-icon" href="/icons/apple-180.png">
<link rel="apple-touch-startup-image"
media="(device-width: 393px) and (device-height: 852px)
and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)"
href="/splash/1179x2556.png">
<meta name="theme-color" content="#1f4e79" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#0b1f33" media="(prefers-color-scheme: dark)">
Desktop apps have no splash screen: the window shows background_color until the first paint, so match it to your page background, in dark mode too.
Check your understanding · Exercise 6
Your Android icon shows as a small logo on a white circle. Which fixes help? Select all that apply.
Platform differences and app capabilities
The standards are shared, but what an installed web app can do depends on the browser engine and the operating system. Chromium on Android and on desktop is the most capable. Safari on iOS is the most limited, and it matters most, because on iOS every browser has to use Safari's engine, WebKit (the EU allows other engines since iOS 17.4, but in practice almost all iOS browsers still use WebKit).
What is different on iPhone and iPad
- Separate storage. A Home Screen web app has its own cookies,
localStorage, IndexedDB, and caches, apart from Safari. A user who signed in to Safari is signed out in the installed app and must sign in again. Plan for that moment: make sign-in quick, with passkeys or a magic link that opens inside the app. - Storage eviction. In the Safari browser, if the user has not interacted with a site during 7 days of Safari use, Safari deletes the storage its scripts wrote. A Home Screen web app keeps its own day counter, which resets each time the user opens it, so WebKit does not expect its data to be deleted. That is one more reason to encourage installs on iOS; still handle missing data, as the client storage lesson explains.
- Push only after install. Web push works on iOS 16.4 and later, only for Home Screen apps, and the permission request must follow a tap inside the installed app. The push notifications lesson covers it in full.
- No background work. There is no Background Sync, no Periodic Background Sync, and no Background Fetch. Writes queued offline are sent the next time the user opens the app.
- No install event. No
beforeinstallpromptand noappinstalled. Usenavigator.standaloneor thedisplay-modemedia query to tell whether the app is running installed.
Capabilities that make it feel native
Beyond install and offline, a set of APIs lets an installed app plug into the operating system. Most started in Chromium and support elsewhere is partial, so check current support tables before you promise one.
| Capability | What it does | Where it works (at the time of writing) |
|---|---|---|
App shortcuts (shortcuts) | Deep links on long-press or right-click of the icon | Chromium on Android and desktop; Safari Dock apps on macOS |
Badging (navigator.setAppBadge) | A number or dot on the app icon | Chromium desktop; Safari Home Screen and Dock apps (needs notification permission on iOS) |
Share target (share_target) | The app appears in the OS share sheet and receives text, links, or files | Chromium on Android and ChromeOS |
File handling (file_handlers) | The OS opens files of a type, such as .csv, in the app | Chromium desktop |
Protocol handlers (protocol_handlers) | Links like web+gym: open in the app | Chromium desktop |
| Window Controls Overlay | The app draws into the title bar area | Chromium desktop |
| Bluetooth, USB, Serial, NFC | Talk to hardware from the page | Chromium only; NFC only on Android; none on iOS |
Every one of these must be feature-detected, never assumed from the user agent: the badge appears where the platform can show it, and the app works the same everywhere else.
async function showUnread(count: number) {
if (!("setAppBadge" in navigator)) return; // no badge here, the in-app counter is enough
try {
if (count > 0) await navigator.setAppBadge(count);
else await navigator.clearAppBadge();
} catch {
// Not installed, or permission missing: ignore
}
}
if ("launchQueue" in window) {
// File Handling: files the OS opened with this app
window.launchQueue.setConsumer(async ({ files }) => {
for (const handle of files) importCsv(await handle.getFile());
});
}
launchQueue may also need a type declaration, since it is not in every TypeScript DOM version.Check your understanding · Exercise 7
A user installs your PWA from Safari on an iPhone. Which of these should you expect? Select all that apply.
App stores and real products
A PWA does not need an app store, but many users look for apps there first, and some companies need a listing for search, reviews, or device management. The packaging is thin: the store app is a wrapper that opens your site, so one deploy still updates every copy.
Google Play: Trusted Web Activity
A Trusted Web Activity (TWA) is an Android app whose only job is to open your PWA full-screen in the user's browser, usually Chrome, with no browser UI. Because it runs in the real browser, it shares the browser's engine, cookies, and service worker with the website, and gets every web feature Chrome has.
The "trusted" part is a two-way check called Digital Asset Links. The Android app declares the website, and the website declares the app in a file at /.well-known/assetlinks.json with the app's package name and the SHA-256 fingerprint of its signing key. If the check passes, the app opens with no address bar. If it fails, for example because Play App Signing changed the key and the file still lists the upload key, the app shows a URL bar at the top, and users notice at once.
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.northsidegym.app",
"sha256_cert_fingerprints": ["14:6D:E9:83:...:A5"]
}
}]
https://gym.example/.well-known/assetlinks.json. Use the fingerprint of the key Google Play signs with, shown in the Play Console.Google's Bubblewrap command-line tool reads your manifest and generates the Android project for a TWA. Store rules still apply to the wrapped app: for example, selling digital goods inside a Play Store app generally means using Play's billing.
Microsoft Store, App Store, and PWABuilder
- Microsoft Store accepts PWAs as first-class apps. The listing installs the PWA through Edge, and it shows up in the Start menu like any Windows app.
- Apple App Store has no PWA path. You need a native app, which can be a thin shell around a
WKWebView. Apple's review guidelines reject apps that are only a repackaged website, so the wrapper must add real native value, such as native push, widgets, or deep OS integration. - PWABuilder, an open-source project started by Microsoft, takes a URL and generates packages for Google Play (a TWA), the Microsoft Store, and an iOS wrapper project. It also reports what the manifest is missing.
In real products
The well-known PWA case studies are from 2017 to 2019, when big brands rebuilt their mobile sites. The numbers below are as the companies reported them; treat them as the scale of the effect, not as a promise.
- Starbucks built a PWA for ordering. It was reported at about 233 KB, roughly 99.8% smaller than its iOS app at the time, and it works offline for browsing the menu and building an order. Starbucks reported about twice as many daily web users after the launch.
- Pinterest rebuilt its mobile website as a PWA and reported that time spent of 5 minutes or more grew by about 40%, and core engagements by about 60%.
- Twitter Lite (now X) launched as a PWA in 2017, a fraction of the size of the native Android app, aimed at slow networks and cheap phones.
- Uber rebuilt
m.uber.comso that the core app is about 50 KB gzipped and loads in a few seconds even on 2G networks, to reach riders in new markets.
The common thread is reach. Each company already had native apps. The PWA served the users the native app did not reach: people on low-end phones, with little storage, on slow or expensive data, or who would not install anything to try the product once.
Check your understanding · Exercise 8
Your TWA in the Play Store suddenly shows a URL bar at the top. What is the most likely cause? Select all that apply.
Failure modes and edge cases
Most PWA bugs are not crashes. They are small breaks in the illusion of being an app: a URL bar that appears, a white flash, a sign-in loop, an icon that never updates. Users rarely report them; they just stop opening the app.
The installed app does not open offline
The team tests offline in a browser tab and it works, but the installed app fails. The cause is almost always start_url: the manifest says /app?source=pwa, the service worker precached /app, and the cache lookup matches the full URL including the query, so it misses. Fix it by precaching the exact start_url, by serving the shell for every navigation in scope (the usual app shell setup), or with ignoreSearch: true on that one lookup.
Leaving scope, and sign-in loops
Any navigation outside scope leaves the app experience: Chromium shows a toolbar with the URL, and iOS opens an in-app browser sheet. Sign-in is where this hurts most, because the app sends the user to auth.example.com, outside scope. On iOS the sign-in may complete in a browser context that does not share cookies with the Home Screen app, so the app still thinks the user is signed out and sends them back, in a loop.
- Keep sign-in pages on the app's own origin and inside
scope, for example by proxying the identity provider through/auth. - Prefer flows that finish in the same context: passkeys, one-time codes typed into the app, or a magic link that opens a URL inside scope.
- Test sign-in in the installed app on a real iPhone, not only in a desktop browser tab.
Manifest changes that do not arrive
You change the theme color and the icon, and installed apps still show the old ones a week later. That is expected: each platform has its own update path, and none is instant.
Chromium fetches the manifest again when the installed app is launched and applies most changes, such as colors, without asking. A change to the name or icon is security-sensitive, since a malicious update could pose as another app, so Chrome holds it until the user reviews and accepts it. Since Chrome 144 it compares icon URLs, not pixels, so give a new icon a new URL.
On Android the WebAPK is rebuilt, which can take a day or more, since Chrome waits for the app to close and the phone to be charging on Wi-Fi. On iOS, the name and icon never update: the user has to remove the app and add it again.
More traps, in one table
| Symptom | Cause | Fix |
|---|---|---|
| Two icons for the same app | No id, then start_url changed | Set id on day one; keep it forever |
| Manifest fails to load for signed-in users only | Manifest is behind auth and fetched without cookies | Serve it publicly, or add crossorigin="use-credentials" to the link |
| White flash before the first paint | background_color does not match the page, often in dark mode | Match it, and set theme-color per color scheme |
| Users stuck on a screen in the iOS app | standalone hides the browser's back button and iOS has no system back | Show an in-app back button when display-mode: standalone matches |
| Content under the iPhone notch or home bar | Full-screen layout without safe-area padding | viewport-fit=cover plus env(safe-area-inset-top) and friends |
| Old version for days after a deploy | The new service worker waits for all windows to close | An update prompt; see the service worker lesson |
To debug, start with Chrome DevTools, Application, then Manifest. It lists every field it parsed, shows the icons with a maskable preview, and prints the exact reason the page is not installable. Lighthouse used to have a PWA category, but it was removed in Lighthouse 12 in 2024, so do not look for a PWA score there.
Check your understanding · Exercise 9
Your installed app shows an offline error on launch, but the same page works offline in a tab. Which are likely causes? Select all that apply.
When a PWA beats a native app, and when it does not
This is a product decision, not a technology contest. Start from who the users are, how they find the product, and which device features the core flow needs.
Where a PWA wins
- Reach from a link. A search result or a shared link opens the full app with no install step. Every step of a store install (open the store, wait for the download, open, sign in) loses a share of users.
- Size. A PWA downloads what it needs, often a few hundred KB, where native apps are commonly 50 MB or more. That matters on cheap phones with little free storage and on prepaid data.
- Release speed. Deploy, and every user gets the new version on their next launch. No store review, and no users stuck on a version from two years ago.
- One code base for the web, Android, desktop, and iOS, owned by one team.
- Occasional use. Apps people open a few times a year, such as parking, event tickets, or a hotel's guest app, are rarely worth an install, but a home screen icon after the first use is welcome.
Where native wins
- iOS-heavy features and users. Background location, background tasks, widgets, Bluetooth or NFC on iPhone, HealthKit, or Siri. And daily power users on iPhone, where push, storage, and background work are all weaker for web apps.
- Heavy compute and media. Advanced camera pipelines, on-device video editing, or high-end 3D, where native APIs are faster and more complete.
- Store-driven discovery. Markets where users find apps mostly by searching the App Store or Play Store.
Decision How should a new product reach mobile users?
What we pick: For most new consumer products and almost all internal tools, start with a PWA. Add native apps when the data shows heavy daily users on iOS or a core feature the web cannot provide. Many large companies run both: the PWA for reach, the native app for depth.
Using it in a system design interview
Bring PWAs up when the prompt mentions mobile users, poor networks, emerging markets, offline use, or "should we build an app". Do not bring it up for a desktop-only admin console that is always online. Draw the three layers (HTTPS, manifest, service worker), put the app shell and cached data on the client side of your diagram, and say what installing adds.
- Junior: know what the manifest and service worker each do, and that installing gives an icon, a window, and offline launch.
- Mid-level: name the install criteria, the app shell, maskable icons, and the iOS limits (push only after install, separate storage, no Background Sync).
- Senior: frame PWA versus native as a product trade-off with data, plan the install prompt and its metrics, handle
idandscopeso the app survives URL changes, and mention TWA and store options.
Check your understanding · Exercise 10
For which products is a PWA a strong first choice? Select all that apply.
Summary
- A PWA is a website plus three things: HTTPS for trust, a manifest for installability, and a service worker for a fast, offline launch.
- Set
idon day one, keepstart_urlinsidescope, and keep sign-in inside scope too, or installed users meet duplicate icons and URL bars. - Chrome installs a site with a valid manifest (name, 192 and 512 icons,
start_url, an appdisplaymode) on HTTPS after some engagement; a service worker is no longer required, but still needed to work offline. beforeinstallpromptis Chromium-only. Safari installs through Add to Home Screen on iOS and Add to Dock on macOS, so give iOS users instructions instead of a button.- The app shell draws the frame from cache in a few hundred milliseconds; the splash screen only hides the launch.
- Ship separate
anyandmaskableicons, with the logo inside the central 80% circle. - iOS is the limiting platform: separate storage from Safari, push only after install, no background sync. Feature-detect every capability.
- A PWA wins on reach, size, and release speed; native wins on deep device features and iOS background work. Many products run both.
In the interview
Questions interviewers ask about this. Answer each out loud first, then compare.
Question 1
What makes a website a progressive web app, and what does each part do?
Three things on top of a normal site. HTTPS, because installing and service workers need a secure origin. A web app manifest, a JSON file with the name, icons, start_url, scope, id, and display mode, which makes the site installable and controls how the installed window looks. A service worker, which serves the app shell and data from a cache so the app opens fast and works offline.
They are separate: Chrome can install a site with a manifest and no service worker, and a site with a service worker but no manifest works offline, but Chromium will not offer to install it.
Question 2
Your PM wants an "Install our app" banner on the first page view. What do you suggest?
Wait for a moment of value: a second visit, a finished booking, a saved item. A banner on arrival is mostly dismissed and takes space from what the user came for.
In Chromium, catch beforeinstallprompt, call preventDefault(), keep the event, and call prompt() from our own button. On iOS there is no prompt API, so show short Share, then Add to Home Screen instructions. Hide all install UI when display-mode: standalone matches, and measure with userChoice and appinstalled.
Question 3
After installing the PWA on iPhone, users say they are signed out and some never get notifications. Why?
Home Screen web apps on iOS have their own cookies and storage, separate from Safari, so the Safari session does not carry over. Make sign-in quick and keep it inside the app's scope, since a sign-in that ends in another context can loop.
Web push on iOS works only from 16.4, only for Home Screen apps, and the permission prompt must follow a tap inside the installed app. Users who only browse in Safari cannot receive it at all.
Question 4
We changed start_url to add a tracking parameter and now some users have two icons. What happened and how do we prevent it?
With no id in the manifest, browsers use start_url as the app's identity, so the new start_url looked like a new app. Set id to the old identity (the old start_url path, here /) so existing installs keep matching, and never change id again.
Also check the service worker precaches the new start_url or serves the shell for every in-scope navigation, or the installed app will fail to open offline.
Question 5
A startup asks: PWA or native apps first?
Start from users and features. If people arrive from links and search, the product changes weekly, and the core is forms, lists, media, and payments, a PWA reaches everyone with one team and no store reviews. It can still be listed in the Play Store as a Trusted Web Activity and in the Microsoft Store.
If the core needs iOS background location, Bluetooth on iPhone, widgets, or the market lives in the App Store, go native or wrap the web app. A common path is PWA first, then native apps for daily power users once usage data justifies it.
Practice it
These design problems lean on progressive web apps.
References
- MDN: Progressive web apps Guides and reference for the manifest, installing, and capabilities.
- MDN: Web app manifest reference Every manifest member with browser support.
- web.dev: What does it take to be installable? Chrome's current install criteria.
- Chrome for Developers: A better way to update your web apps How Chrome 144 applies manifest changes and asks before changing the name or icon.
- WebKit: Features in Safari 26.0 Every site added to the Home Screen opens as a web app on iOS 26.
- web.dev: Learn PWA A full course on PWAs, including the app shell and installation.
- W3C: Web Application Manifest The specification.
- Chrome for Developers: Trusted Web Activity Packaging a PWA for Google Play.
- PWABuilder Generates store packages from a PWA's URL.
Read next
- Service worker cachingUsing a service worker as a programmable cache between your page and the network, so you choose per request whether to answer from a stored copy, the network, or both: the basis of instant loads and offline apps.
- Web push notificationsMessages your server sends through the browser's push service, which wake your service worker to show a notification even when your site isn't open: powerful for re-engagement, and easy to ruin with a bad permission prompt.
- Client storage optionsThe six places a web app can keep data on the user's device (cookies, localStorage, sessionStorage, IndexedDB, the Cache API, and OPFS), how big, how fast, and how long-lived each one is, and how to pick the right one for each kind of data.
- Offline-first designA way of building apps where the device's own copy of the data is what the UI reads and writes, and a sync engine reconciles it with the server in the background, so the app stays fast and usable on a bad connection or none at all.
- Background sync and queued writesSaving the user's writes in a durable queue when the network is gone, then sending each one exactly once when it comes back, even if the tab was closed: the write half of every offline-capable app.