Skip to content

Progressive Web App FAQ

This FAQ answers the questions developers most often ask about Progressive Web Apps, grouped by topic: general concepts, iOS and Safari, installation, offline and caching, push, SEO, performance, app stores and security. Each answer is short enough to read in a minute and states the behavior as of September 2026, with the browser and version caveats that matter. Every answer links to the page on this site that explains the mechanism, the code and the edge cases in depth.

Key takeaways

  • A PWA is a website with a web app manifest, HTTPS and (usually) a service worker. It stays a website, and installation and offline support are enhancements.
  • The core (service workers, Cache Storage, IndexedDB, Web Push, manifests) works in Chromium, Safari and Firefox. Install prompts and background APIs are Chromium-only.
  • On iPhone and iPad every browser uses WebKit, installation is manual, and push works only in Home Screen web apps.
  • A service worker is no longer required for Chromium's install criteria, but you still need one for offline support and push.
  • Search engines index PWAs like any other site. Crawlers don't keep service worker state between page loads, so serve real HTML from your server.
  • You can ship a PWA to Google Play (Trusted Web Activity) and the Microsoft Store with little or no code change. The Apple App Store requires a native wrapper that adds real app value.

Jump to: General · iOS & Safari · Installation · Offline & caching · Push notifications · SEO · Performance · App stores · Security

General questions

What is a Progressive Web App?

A Progressive Web App (PWA) is a website that uses modern web platform features to behave like an installed application. Three pieces make it work: a web app manifest that describes the app's name, icons, start URL and display mode; a service worker that intercepts network requests so the app can load offline and receive push messages; and HTTPS, which both require. On top of that, PWAs can use device and OS integration APIs such as Web Share, badging and file handling where the browser supports them. The term was coined in June 2015 by Alex Russell and Frances Berriman. The word "progressive" is the key idea: the same URL works as a normal website in any browser and gains app capabilities where they exist. Read What Is a PWA? for the full definition and History & Evolution for how the idea developed.

Is a PWA a website or an app?

Both, and that's the point. A PWA has a URL, is indexed by search engines, can be linked to and runs in any browser tab. Once installed, it gets its own window or full-screen view, an icon on the Home Screen, taskbar or Dock, an entry in the app switcher and, on some platforms, OS integration such as share targets and file associations. On Android, Chrome even generates a real Android package (a WebAPK) for it. Unlike a native app, the code isn't downloaded from a store as a signed bundle: it's fetched from your server, cached by the service worker and updated whenever you deploy. That makes updates instant, but it also means the app is bound by the browser engine's capabilities and security model. PWA vs Native vs Hybrid compares the models in detail.

What is the minimum a site needs to count as a PWA?

There is no official certification, but in practice you need:

  • HTTPS for every page (localhost is exempt during development).
  • A web app manifest linked from every page, with name or short_name, start_url, display set to standalone (or another app-like mode; window-controls-overlay is valid only in display_override), and at least one purpose: "any" icon (PNG, SVG or WebP) of at least 144 px, the Chromium floor. Providing 192 px and 512 px icons is recommended. Setting id explicitly is strongly recommended.
  • A service worker with a fetch handler that makes at least the start URL and an offline fallback page work without the network.

That is enough to be installable in Chromium, look right when added to the Home Screen on iOS, and open offline. The Tutorial: Your First PWA builds exactly this, and the Installability Criteria page lists each browser's exact checks. For a release checklist that goes beyond the minimum, see the Production Checklist.

Which browsers support PWAs in 2026?

All major engines support the core, but they differ sharply above it. Support data as of September 2026; check MDN and caniuse for live data.

Capability Chromium (Chrome, Edge, Samsung Internet) Safari (macOS, iOS, iPadOS) Firefox
Service workers, Cache API, IndexedDB ✅ ✅ ✅
Web Push ✅ ✅ (iOS: Home Screen apps only) ✅
Install from browser ✅ with beforeinstallprompt ✅ manual (Add to Home Screen, Add to Dock) ⚠️ Android shortcut; Windows (143+) on desktop
Background Sync, Periodic Sync, Background Fetch ✅ ❌ ❌
File handling, protocol handlers, WCO ✅ desktop ❌ ❌

The Platform Support section has the full feature matrix with version numbers for seven browsers.

Did Lighthouse removing its PWA category mean PWAs are deprecated?

No. Lighthouse 12.0, released in April 2024, removed the PWA category and its badge because Chrome's installability criteria had changed (a service worker with a fetch handler was no longer required) and the checks no longer reflected what makes a good PWA. The underlying technologies weren't affected. Since then browsers have kept shipping PWA features: Safari's Declarative Web Push, iOS 26 opening every Home Screen site as a web app, Firefox's desktop web apps on Windows and Chromium's static routing. Installability is now checked in DevTools' Application panel, and you audit the rest (offline behavior, update flow, manifest quality) with your own tests. Lighthouse & Auditing describes a replacement audit process, and Automated Testing shows how to test service workers in CI.

Do I need a framework or Workbox to build a PWA?

No. A PWA is built from standard browser APIs: a JSON manifest and a service worker written in plain JavaScript are enough, and the first PWA tutorial uses no libraries. Tooling becomes valuable as the app grows. Workbox generates a precache manifest with content hashes at build time, which is tedious and error-prone by hand, and provides tested caching strategies, expiration and background sync queues. Build plugins such as the Vite PWA plugin and the framework integrations for Next.js, Nuxt, SvelteKit, Angular and others wire Workbox into your build. The rule of thumb: hand-write the service worker while you learn the lifecycle, then adopt Workbox for precaching before you ship anything with more than a handful of assets.

When should I build a native app instead of a PWA?

Build native, or a hybrid, when your core use case depends on a capability the web doesn't give you on your users' platforms. Typical examples: reliable background execution (location tracking, audio recording while locked, scheduled background work on iOS), home screen widgets, Live Activities, deep integration with system assistants, Bluetooth or NFC on iPhone, or health and payment wallet APIs. Distribution can also decide it: if your users only look for apps in the App Store, a PWA alone won't reach them. A PWA is usually the better first choice when reach, linkability, fast iteration and one codebase matter more than those capabilities. Many teams ship a PWA and wrap it for stores later. When to Build a PWA is a decision guide, and PWA vs Native vs Hybrid compares capabilities one by one.

iOS and Safari

Do PWAs work on iPhone and iPad?

Yes, with WebKit's feature set and Apple's rules. Service workers, Cache Storage, IndexedDB, the manifest (name, icons, display, start_url, id, scope) and the display-mode media query all work. Users install through the Share sheet (Add to Home Screen), in Safari or, since iOS 16.4, in other browsers. Installed apps get Web Push, notifications and app badging (iOS 16.4 and later), and their storage is separate from Safari's. What's missing: beforeinstallprompt, Background Sync, Periodic Background Sync, Background Fetch, Web Bluetooth, WebUSB, File Handling, share targets and the maskable-icon and splash-screen handling Android does from the manifest. Every browser on iOS uses WebKit, so Chrome or Edge on iPhone don't add those features. iOS & iPadOS documents every difference.

Why doesn't my PWA show an install prompt on iOS?

Because iOS has no programmatic install prompt. Safari never fires beforeinstallprompt, doesn't show an automatic install banner, and doesn't expose any API to trigger Add to Home Screen. The user has to open the Share sheet and choose Add to Home Screen themselves. What you can do is detect iOS in a browser tab (not already running standalone), and show your own short instructions at a moment when installing makes sense, such as after the user has completed a task or asked for notifications. Since iOS 26, the location of the Share button depends on the user's tab layout, so describe the steps rather than drawing an arrow to a fixed spot. Never show those instructions inside the installed app. Install Prompts & Custom UI includes a complete iOS instructions component.

Does web push work on iOS?

Yes, since iOS and iPadOS 16.4, but only for web apps added to the Home Screen. In a Safari tab, Notification is undefined and pushManager.subscribe() can't be used. Inside a Home Screen web app, you must request permission from a user gesture (a click handler), use a VAPID key, and show a notification for every push: WebKit revokes the subscription after repeated pushes that don't display one. Apple's push service uses the standard Web Push protocol with endpoints on push.apple.com, so the same server code works for Safari, Chrome and Firefox. Safari 18.4 on iOS also added Declarative Web Push, which shows a notification from a JSON payload without running your service worker. Web Push on iOS & Safari covers setup, error codes and debugging on a device.

What changed for web apps in iOS 26?

The biggest change: according to WebKit's WWDC25 announcement, "by default, every website added to the Home Screen opens as a web app." Before iOS 26, a site without a manifest display of standalone (or the old apple-mobile-web-app-capable meta tag) was added as a bookmark that opened in Safari. Now the Add to Home Screen sheet has an Open as Web App switch that is on by default, and the user can turn it off. The manifest still matters: it supplies the name, icons, start_url, scope and id, and offline support still depends on your service worker. The practical effect is that any site can end up running as a standalone app on iOS, so test your site in that mode even if you never planned for it. See iOS & iPadOS and Installation by Platform.

Does Safari delete my PWA's data after seven days?

Not for Home Screen web apps. Safari's Intelligent Tracking Prevention deletes all script-writable storage (IndexedDB, Cache Storage, localStorage, service worker registrations) of a site after seven days of Safari use without user interaction with that site. WebKit's announcement of the rule stated that web apps added to the Home Screen "are not part of Safari and thus have their own counter of days of use," and that WebKit does not expect their first-party data to be deleted. In a Safari tab, the rule applies, so a PWA used only in the browser can lose its offline data. Home Screen apps can still be evicted under genuine storage pressure, like any origin; requesting navigator.storage.persist() improves your odds. Storage Quotas & Persistence has the details for every engine.

Does an installed iOS web app share storage and login with Safari?

No. A Home Screen web app on iOS and iPadOS has its own storage container: IndexedDB, Cache Storage, localStorage and service worker registrations are separate from Safari's, and so are notification permission and push subscriptions. When the app is created, Safari copies the site's cookies into it (since Safari 17.2), so a user who was signed in with a cookie usually stays signed in, but client-side data starts empty. Each copy of the app on the Home Screen is a separate app with its own data. Dock web apps on macOS behave the same way. Design your sign-in and sync so that a second, empty copy of your app isn't a broken copy: fetch user data from the server on first launch, and never assume that a subscription made in Safari carries over. See iOS & iPadOS.

Can iOS browsers use Chromium or Gecko instead of WebKit?

Only in two regions, and not for Home Screen web apps. App Store Review Guideline 2.5.6 requires apps that browse the web to "use the appropriate WebKit framework and WebKit JavaScript," with an entitlement to use an alternative engine available in the EU (under the Digital Markets Act, from iOS 17.4) and Japan (from iOS 26.2). Browser vendors have built prototypes, but Open Web Advocacy reported in June 2026 that no vendor had shipped an alternative-engine browser to users. Home Screen web apps continue to run on WebKit regardless of which browser created them. So Chrome, Edge and Firefox on iPhone have Safari's PWA capabilities, not those of their desktop or Android versions. Platform Support explains the rules and their history.

Installation

Is a service worker required to make a PWA installable?

Not in Chromium any more. Chrome stopped requiring a service worker with a fetch handler for installation from the browser menu in Chrome 108 on Android and Chrome 112 on desktop. At that point the automatic install prompt still required a fetch handler, but current Chromium's install promotion has no service worker check, so beforeinstallprompt fires for a site with a valid manifest and no service worker. Safari never required one, and since iOS 26 any site can be added as a web app. That doesn't make the service worker optional for a good PWA: without one your app shows the browser's offline error page when the network is down, can't receive push messages and can't precache its shell for instant startup. Users who install an app expect it to open without a connection. Installability Criteria lists every browser's current checks, and Offline UX & Fallbacks shows the minimum offline experience to build.

Why isn't beforeinstallprompt firing?

Work through these causes in order:

  1. The browser doesn't support it. Only Chromium browsers fire it. Safari and Firefox never do.
  2. The app is already installed in this browser profile.
  3. The manifest fails a check, or points users elsewhere. Open DevTools, Application > Manifest, and read the installability section. The usual culprits are a missing purpose: "any" PNG, SVG or WebP icon of at least 144 px with declared sizes, a display of browser, a start_url outside the manifest's origin, a manifest blocked by manifest-src in your CSP, or prefer_related_applications: true, which tells Chromium on Android to promote your native app instead (desktop Chromium ignores it unless related_applications lists a chrome_web_store entry, or a play entry on ChromeOS with Android apps).
  4. You're in an incognito or guest window.
  5. Your listener is attached too late. The event normally fires shortly after load, once per document (and again after a dismissal or a back/forward cache restore), and if nobody is listening it's gone until the next page load. Register the listener in a small classic script in <head>, not in a lazily loaded module.
  6. You are waiting for the automatic install UI. On Android, Chrome suppresses its own install message for 90 days after a dismissal. The event itself still fires, so offer your own install button instead.

Install Prompts & Custom UI walks through each case and shows a funnel instrumented with analytics.

How do I detect whether my PWA is installed or running standalone?

Check how the current page is displayed, not whether an install exists:

display-mode.js
export function isRunningInstalled() {
  // Apple platforms first: true in iOS/iPadOS Home Screen web apps and macOS Dock web apps.
  // iOS reports a manifest "standalone" app as display-mode: fullscreen (WebKit bug 264218)
  // and an app added without a manifest as "browser", so the media query alone misses it.
  if (navigator.standalone === true) return true;
  // Standard: works in Chromium, Firefox and Safari. Include every app-like mode you use.
  const modes = ["fullscreen", "standalone", "minimal-ui", "window-controls-overlay"];
  return modes.some((m) => matchMedia(`(display-mode: ${m})`).matches);
}

// The applied mode can change at runtime (for example, a desktop app window moved into a tab).
export function onDisplayModeChange(callback) {
  const modes = ["fullscreen", "standalone", "minimal-ui", "window-controls-overlay", "browser"];
  const lists = modes.map((m) => matchMedia(`(display-mode: ${m})`));
  const handler = () => callback(isRunningInstalled());
  lists.forEach((mql) => mql.addEventListener("change", handler));
  // Return an unsubscribe function so components can clean up.
  return () => lists.forEach((mql) => mql.removeEventListener("change", handler));
}

To learn whether the app is installed while the user is in a browser tab, Chromium offers navigator.getInstalledRelatedApps(), which needs your manifest's related_applications to point at the web app itself. appinstalled tells you when an install happens, in Chromium only. There's no way to detect an iOS install from Safari. Because the display mode can change at runtime (a desktop user can move an app window into a browser tab), listen for change events on the media query lists, as onDisplayModeChange() does, rather than checking once. Detecting Installed Apps covers all methods and their limits.

Can users install PWAs on desktop computers?

Yes, on every major desktop OS, with differences. Chrome and Edge install PWAs on Windows, macOS, Linux and ChromeOS from an icon in the address bar or the menu, and you can offer your own button through beforeinstallprompt. Installed apps get their own windows, taskbar or Dock icons, and OS integration such as file handling, protocol handlers, app shortcuts and Window Controls Overlay. Safari 17 and later on macOS Sonoma can add any site to the Dock through File > Add to Dock. Firefox 143 (September 2025) added web apps pinned to the Windows taskbar, enabled by default, with no install event and limited manifest support; Firefox 150 extended them to the Microsoft Store build. On Linux the feature exists but is disabled by default behind browser.taskbarTabs.enabled, and Firefox on macOS has no equivalent. Microsoft Edge additionally lets you distribute through the Microsoft Store. Desktop Platforms documents each one.

How do I update my app's name or icon after users install it?

It depends on the browser. Chrome on desktop, since Chrome 144, checks for manifest updates whenever a page linking a manifest with the same id loads; ordinary members apply silently, while name and icon changes wait for the user to review them from the app menu. Chrome on Android checks when the WebAPK is launched, asks the user to confirm a name change, and regenerates the WebAPK in the background at most about once a day (while charging and on an unmetered network). Safari and Firefox document no update mechanism: they keep the metadata captured at install time, and iOS never updates a Home Screen icon after it has been added. Two rules help everywhere: set a stable id so the browser recognizes the app as the same one, and give each new icon a new URL (for example with a content hash), because desktop Chromium compares icon entries (URL, size, purpose) rather than downloading and comparing bytes. When the entries do change, desktop Chromium downloads the new icon and applies it silently if the images differ by less than 10%; bigger changes wait for review. App Identity & Updates documents the update algorithm per browser.

What is a WebAPK, and why does it matter?

A WebAPK is a real Android application package that Google's servers generate and sign when a user installs a qualifying PWA from Chrome on Android (Samsung Internet does the same on Samsung devices). Unlike a home screen shortcut, a WebAPK appears in the app drawer and in Android's app settings, has its own entry in the recent-apps list, can register as a share target through the manifest's share_target, and captures links to URLs in its scope through Android intent filters. Chrome keeps it in sync with your manifest by minting a new version when relevant members change. It still runs your code in Chrome, sharing Chrome's storage for your origin. Other Android browsers, such as Firefox, create shortcuts instead. Android covers WebAPK minting, updates and link capturing.

Offline and caching

How does a PWA work offline?

The service worker sits between your pages and the network. On install, it downloads and stores the files the app needs to start (the app shell, scripts, styles, an offline fallback page) in Cache Storage. Afterwards, every request from a controlled page fires a fetch event in the worker, which can answer from the cache, from the network, or from both according to a caching strategy. Structured data such as documents, messages or an outbox of pending changes goes in IndexedDB. When the network is unavailable, the worker answers from those stores; when nothing is cached for a request, it returns your offline fallback instead of the browser's error page. Offline UX & Fallbacks covers what to show users, and Offline-First Data & Sync covers writing data while offline.

Which caching strategy should I use?

Choose per type of request, not per app:

Request type Strategy Why
Fingerprinted JS, CSS, fonts (app.3f9a1c.js) Precache or cache-first The URL changes when the content does
HTML pages Network-first with a timeout, cached fallback Content must be fresh, but still load offline
API reads that may be slightly stale Stale-while-revalidate Instant response, refreshed for next time
Avatars, thumbnails, third-party images Cache-first with expiration and entry limits Unbounded otherwise
POST, PUT, DELETE, auth, payments Network-only Never cache mutations or credentials

Never use cache-first for HTML or unversioned URLs: users will see old content indefinitely. Caching Strategies implements each strategy in plain JavaScript and Workbox, with timeouts and error handling.

How much data can a PWA store?

A lot, and the limit is per origin, shared by IndexedDB, Cache Storage, OPFS and service worker registrations. Chromium allows an origin up to about 60% of total disk space. Firefox allows the smaller of 10% of disk or 10 GiB per site group for best-effort storage, and up to 50% of disk when storage is persistent. Safari 17 and later allows up to 60% of disk per origin for browser apps (Safari, and Chrome, Edge or Firefox for iOS, which can be the default browser), but only 15% in non-browser apps that embed WKWebView. navigator.storage.estimate() reports usage and quota, though Chrome reports quota as usage plus 10 GiB (the default since Chrome 148, to stop the value from revealing disk size), not the enforced limit. Data is best-effort by default and can be evicted under storage pressure; navigator.storage.persist() asks the browser to keep it. Storage Quotas & Persistence has the details.

Why do users still see the old version after I deploy?

Because service workers are deliberately conservative about updates. The browser checks for a new worker on navigation, compares the script byte for byte, and installs the new version in the background. The new version then waits until every tab using the old one is closed, and reloading one tab isn't enough. Meanwhile, the old worker keeps serving its precached files. Three things usually go wrong: the worker script itself didn't change (so no update is detected), sw.js or the HTML is served with a long Cache-Control: max-age from the HTTP cache or a CDN, or the new worker is stuck in waiting. The fix is to inline a precache manifest with content hashes into the worker, serve sw.js and HTML with no-cache, and show an "update available, reload" prompt that calls skipWaiting(). Updating Service Workers has the full pattern.

Should I use Cache Storage, IndexedDB or localStorage?

Use each for what it's designed for:

  • Cache Storage stores HTTP Request/Response pairs. Use it for anything you'd fetch by URL: pages, scripts, images, API responses you replay verbatim.
  • IndexedDB stores structured JavaScript values with indexes and transactions, and it's available in workers. Use it for app data, sync queues and anything a service worker must read or modify.
  • OPFS stores files and supports fast synchronous access from workers. Use it for large binary data and databases like SQLite compiled to WebAssembly.
  • localStorage is synchronous, string-only, limited to about 5 MiB per origin and unavailable in service workers. Use it only for tiny UI preferences.

All but localStorage share the origin's quota and are evicted together. IndexedDB, Cache Storage API and Origin Private File System cover each API in depth.

Can a PWA sync data in the background?

Partly, and only in Chromium. Background Sync lets a service worker retry sending queued changes when connectivity returns, even after the user has closed the page. Periodic Background Sync lets an installed app refresh content at intervals the browser chooses (at most every 12 hours in Chromium, scaled by engagement). Background Fetch hands large downloads to the browser. None of these exist in Safari or Firefox, and Mozilla's position on Background Sync is negative. The cross-browser approach is to keep an outbox in IndexedDB and flush it whenever the app starts, the online event fires, the page becomes visible again, or the service worker starts; use Background Sync as an enhancement where it exists. Push can also wake a worker, but it must show a notification every time.

What is an opaque response, and why is my storage usage so high?

An opaque response is what you get when you fetch a cross-origin resource in no-cors mode, for example an image or script from a CDN that doesn't send CORS headers. Its type is "opaque", its status reads as 0, and its headers and body are hidden from your code. That creates two problems. First, you can't tell a successful response from a 404 or 500, so a cache-first strategy may store an error permanently. Second, to avoid leaking the real size across origins, Chromium pads each opaque response in Cache Storage by a pseudo-random amount between 0 and about 14 MiB (about 7 MiB on average), so a few hundred cached third-party images can use gigabytes of quota. Fix it by requesting CORS (crossorigin attributes, mode: "cors") from servers that support it. See Cache Storage API and Storage Quotas & Persistence.

Push notifications

How do push notifications work in a PWA?

Four parties are involved. Your page asks for notification permission and calls registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey }) with your VAPID public key. The browser registers with its vendor's push service and returns a PushSubscription: an endpoint URL plus encryption keys. You send that subscription to your server. To notify the user, your server encrypts a payload with the subscription's keys (RFC 8291), signs a VAPID JWT (RFC 8292) and POSTs it to the endpoint (RFC 8030). The push service delivers it to the browser, which wakes your service worker with a push event, and the worker calls showNotification(). Clicks arrive as notificationclick. Push Notifications has the client and server code, and The Web Push Protocol explains the wire format.

Do I need Firebase or a third-party service to send web push?

No. Web Push is an open IETF protocol, and your server talks directly to whichever push service the browser chose: Google's Firebase Cloud Messaging infrastructure for Chrome, Mozilla's push service for Firefox, Microsoft's for Edge and Apple's for Safari. You don't need an account with any of them. You identify your server with a VAPID key pair that you generate yourself, and a library such as web-push for Node.js or pywebpush for Python handles the encryption and signing. The only thing you don't control is the endpoint URL, which the browser gives you. Hosted notification services add scheduling, segmentation and analytics on top of the same protocol, but they aren't required. Validate that subscription endpoints point to known push service hosts, so your sender can't be abused for server-side request forgery. See The Web Push Protocol.

Can I send silent push messages to update data in the background?

Not portably. Chromium and Safari require you to subscribe with userVisibleOnly: true, which is your promise that every push results in a visible notification, and reject false. Firefox accepts userVisibleOnly: false but gives each subscription a quota of background pushes that show no notification. Browsers enforce the promise differently. Chrome and Edge show a generic "This site has been updated in the background" notification if your worker doesn't show one. Safari removes the subscription after repeated pushes that don't display a notification. You can still refresh data during a push event, as long as you also show a notification. For silent refreshes, Chromium's Periodic Background Sync is the only option, and it only works for installed apps. Push Notifications describes each browser's penalty in detail.

Why do push subscriptions stop working?

Subscriptions end for several reasons: the user revoked notification permission, cleared site data or uninstalled the app; the browser or push service expired or rotated the subscription; Safari revoked it after pushes without notifications; or storage for the origin was evicted. Your server learns about it when the push service responds 404 Not Found or 410 Gone, and it must delete the subscription then. The pushsubscriptionchange event can notify your service worker so it can resubscribe, but support is uneven: Firefox fires it (with oldSubscription and newSubscription from version 137) and so does Safari 16+ on macOS, but it is not available on iOS and iPadOS, and Chrome 138 and later fire it only when notification permission is granted again after being revoked, with both properties null. Robust apps also compare the current subscription with the server's copy each time the app starts, and resubscribe when permission is still granted but the subscription is missing. See Push Notifications.

How large can a web push payload be?

The Web Push protocol guarantees that push services accept an encrypted body of 4096 bytes. After the aes128gcm header (with salt, record size and the server's public key), padding delimiter and authentication tag, the largest plaintext you can send is 3993 bytes. Some services accept more, but you can't rely on it, and Apple's service caps payloads at 4 KB. Larger payloads fail with 413 Payload Too Large. Treat the payload as a notification and a pointer, not as data transport: send the title, body, a URL and an ID, and let the service worker fetch anything larger from your API when it handles the push event or when the user opens the notification. Also set TTL on every request, and use Topic to replace outdated undelivered messages. The Web Push Protocol shows the byte-level layout.

SEO

Are PWAs bad for SEO?

No. Search engines see a PWA as a website, because it is one. Nothing about a manifest or a service worker hurts ranking. What hurts is the architecture some PWAs use: single-page apps that render all content with client-side JavaScript, use fragment URLs (#/page), return 200 for every route including missing ones, or hide content behind interactions. Google renders JavaScript with an evergreen version of Chromium, but rendering is deferred and costs crawl resources, and other search engines and social media previews may not run your JavaScript at all. Server-side rendering or static generation, real URLs with the History API, correct status codes and per-page titles and meta descriptions make a PWA as indexable as any site. SEO for PWAs covers each issue, and SPA vs MPA PWAs explains the architectural trade-offs.

Does Googlebot run my service worker?

Assume it doesn't benefit from it. Google's documentation says its Web Rendering Service "does not retain state across page loads": local storage, session storage and HTTP cookies are cleared between page loads, and Googlebot declines permission requests. A service worker registered during one render is therefore not controlling the next URL Googlebot fetches, so every crawled page must be complete when served by your server or CDN, not assembled from Cache Storage. Two consequences follow. Your offline fallback page should never be what a server returns for a real URL. And content that only exists in your client-side cache won't be indexed. Google also notes that its renderer may ignore caching headers, so version your asset URLs. See SEO for PWAs.

Does having a manifest or being installable improve rankings?

Google hasn't documented any ranking signal for having a web app manifest, a service worker or being installable. The indirect effects are real, though. A service worker that serves repeat visits from cache can improve field performance metrics such as Largest Contentful Paint for returning visitors, and those metrics are part of Google's page experience signals, measured from real Chrome users through CrUX. Conversely, a poorly written service worker (network-first without a timeout, or one that starts on every navigation for no benefit) can make pages slower. Installed users also tend to return directly rather than through search, which doesn't change rankings but does change how much of your traffic search accounts for. Measure rather than assume: see Core Web Vitals and Analytics for PWAs.

How should a single-page PWA handle URLs to stay indexable?

Give every view its own real path (/products/42, not /#/products/42), and update it with the History API or the Navigation API as users move around. Make the server able to render any of those URLs directly, with its own <title>, meta description, canonical link and content in the initial HTML, through server-side rendering, static generation or prerendering. Return a real 404 status for unknown paths rather than serving the app shell with 200. In the service worker, serve the cached app shell as a navigation fallback only for app routes, with a denylist for URLs such as /sitemap.xml, /robots.txt, API paths and authentication callbacks. Use <a href> links rather than click handlers on non-link elements so crawlers can discover pages. SPA vs MPA PWAs and Precaching & Runtime Caching cover the navigation fallback in detail.

Can the service worker change what search engines see?

Only for visitors whose browser has installed it, and that's why you should keep the served HTML and the cached HTML equivalent. Crawlers fetch your URLs without an installed worker, so they see what your server returns. Returning users see what the worker returns. If the two diverge (for example, an old cached app shell that links to pages that no longer exist, or a cached page with outdated structured data), users and crawlers see different sites. That's not cloaking in intent, but it produces confusing results and stale content. Keep HTML network-first, version cached shells with the deploy, and avoid rewriting content in the worker. The Service-Worker-Navigation-Preload header on preload requests lets servers distinguish those requests; if you vary the response on it, send Vary: Service-Worker-Navigation-Preload. See Navigation Preload.

Performance

Does a service worker make my site faster?

It can, for repeat visits, and it can also make it slower. When a page is served from Cache Storage, you remove the network round trip from the most important request, which can dramatically cut Time to First Byte and Largest Contentful Paint, especially on slow or flaky networks. But a service worker is JavaScript that has to start before it can answer. If it isn't already running, the browser boots it first, and if your fetch handler then goes to the network anyway, you've added start-up time for nothing. Navigation preload overlaps start-up with the network request, the Static Routing API skips the worker for requests it doesn't need to handle, and a worker without a fetch listener has no start-up cost for navigations. Measure with real-user data. Loading Performance and Navigation Preload cover the techniques.

How big should my precache be?

As small as it can be while still letting the app start offline. Every precached byte is downloaded on the first visit, in the background but competing with the page for bandwidth and the user's data plan, whether or not the user ever needs it. Precache the app shell, the critical route bundles, fonts used above the fold and an offline fallback page. Cache everything else at runtime, when the user actually requests it, with expiration limits. A precache of a few hundred kilobytes to a couple of megabytes is typical for an app shell. If yours is tens of megabytes, you're probably precaching images, rarely used routes or source maps. Workbox's build tools report the precache size and let you exclude files by pattern. See Precaching & Runtime Caching and App Shell Model.

How do PWAs affect Core Web Vitals?

A service worker mainly affects Largest Contentful Paint, through Time to First Byte (a cache read instead of a network round trip, minus worker start-up) and through faster loading of cached images and fonts. It rarely affects Interaction to Next Paint directly, because it runs off the main thread, but PWA architectures do: heavy client-side rendering, hydration and large JavaScript bundles on the main thread cause slow interactions. Cumulative Layout Shift is affected by how you render cached content and late-arriving data. The thresholds are LCP of 2.5 s or less, INP of 200 ms or less and CLS of 0.1 or less at the 75th percentile. Remember that CrUX covers only Chrome users, so iOS users of your installed app are invisible in Google's data. Core Web Vitals covers each metric for PWAs.

Why are back and forward navigations sometimes slower in my PWA?

Usually because your pages aren't eligible for the back/forward cache (bfcache). A bfcache restore shows the page instantly from memory without touching the service worker or the network. Common reasons a page isn't restored are an unload event listener, Cache-Control: no-store on the document in some browsers, open connections such as WebSockets or IndexedDB transactions, and service worker actions. In Chromium, a new worker calling clients.claim(), or a worker sending postMessage() to a page, evicts that page from bfcache. Check eligibility in Chrome DevTools under Application > Back/forward cache, and monitor pageshow events with event.persisted in your analytics. Avoid broadcasting messages to every client on each activation. See HTTP Caching & Service Workers and Runtime Performance.

How do I measure a PWA's performance in the field?

Collect real-user monitoring (RUM) data from your own users, because lab tools and CrUX miss important cases. Use the web-vitals library's attribution build to record LCP, INP and CLS with the causes behind slow values, and send them with navigator.sendBeacon() or fetch(..., { keepalive: true }). Add dimensions that matter for PWAs: whether the page was controlled by a service worker (navigator.serviceWorker.controller), the display mode (browser tab or installed), whether the response came from cache (Navigation Timing's workerStart and transferSize), and for Static Routing the workerMatchedRouterSource timing field. Report LCP separately for first and repeat visits, since a service worker typically helps only the latter. Outbox analytics sent while offline and flush them later. Measuring Performance and Analytics for PWAs show the full setup.

App stores

Can I publish a PWA on Google Play?

Yes. Google's supported route is a Trusted Web Activity: a small Android app that opens your PWA full-screen in the user's browser (Chrome 72 and later, or another browser that supports the protocol). You prove that you own both the app and the site with a Digital Asset Links file at /.well-known/assetlinks.json; without it, users see a URL bar. The PWA runs in the real browser, with its service worker, cookies, storage and push, so you keep deploying through your web server. Google's Bubblewrap CLI and PWABuilder generate and sign the Android project from your manifest. The listing must follow Google Play's policies like any app, including its rules on payments for digital goods. Publishing to App Stores walks through the whole submission.

Can I publish a PWA to the Microsoft Store?

Yes, and Microsoft documents it as requiring no code changes. You reserve an app name in Microsoft Partner Center (choosing MSIX or PWA app), use PWABuilder to generate a Windows package from your live site using the reserved package and publisher IDs, and upload the resulting .msixbundle and .classic.appxbundle files. Microsoft says review typically takes 24 to 48 hours. The installed app runs in Microsoft Edge's web app runtime, and Edge adds a Referer: app-info://platform/microsoft-store header to the first navigation so you can measure Store installs. Code changes deploy through your server as usual, but manifest changes such as a new name, icon or file_handlers require a new package, because the manifest data is copied into it. See Publishing to App Stores.

Can I publish a PWA to the Apple App Store?

Not directly. Apple has no PWA submission path, so you wrap the web app in a native iOS app that uses WKWebView, with tools such as Capacitor or PWABuilder's iOS project. The hard part is review, not packaging. App Store Review Guideline 4.2 says an app "should include features, content, and UI that elevate it beyond a repackaged website," and apps that are only a website in a frame are routinely rejected under it. Wrapped apps succeed when they add native value: push through Apple's native notification service, native navigation, widgets, offline content, or device integrations the web can't reach on iOS. Also note that a WKWebView app gets a smaller storage quota than Safari and doesn't run your service worker the same way unless you configure app-bound domains. Publishing to App Stores covers the options.

Do I have to resubmit to the stores every time I update my PWA?

Mostly no. In a Trusted Web Activity, a Microsoft Store package or a WKWebView wrapper, your HTML, CSS, JavaScript and service worker are loaded from your server, so deploying to your server updates every store install, just like the web version. You resubmit when the native wrapper changes: a new app name or icon, new Android or Windows integrations (share targets, file or protocol handlers), a new target SDK level required by Google Play, changes to Digital Asset Links, or new native plugins in an iOS wrapper. Microsoft's documentation states explicitly that changes to the web app manifest require a new package, because its data is copied into the Windows package. Remember that each store may still review your app's content and policies periodically, independent of submissions. See Publishing to App Stores.

Can a PWA in an app store take payments?

Yes, subject to each store's rules for digital goods. On Google Play, Google's documentation for Trusted Web Activities states that "if your app is distributed through Google Play, and you want to sell digital goods or offer subscriptions, you must use Google Play Billing"; a TWA does that through the Digital Goods API plus the Payment Request API with the https://play.google.com/billing payment method. Google has alternative billing programs in some regions, so check the current policy for your markets. Physical goods and services can use any payment processor. The same PWA on the open web can use the Payment Request API, Apple Pay in Safari or a web checkout. In the Apple App Store, digital goods sold inside the app are subject to Apple's in-app purchase rules. Payments covers the web APIs, and Trusted Web Activity covers Play Billing.

Security

Why do PWAs require HTTPS?

Because a service worker is a persistent, programmable proxy for your whole origin. It intercepts every request from the pages it controls, keeps working after the page closes, and survives across visits for days or longer. If it could be installed over plain HTTP, anyone on the network path (public Wi-Fi, a compromised router) could inject a malicious worker that keeps controlling your site long after the user left that network. So browsers expose service workers, push, notifications and nearly all capability APIs only in secure contexts: HTTPS pages, plus localhost for development. HTTPS also protects the integrity of the manifest, the worker's update checks and push subscriptions. Use HSTS so users can't be downgraded to HTTP before the worker loads. Service Worker Security explains the full threat model.

Can an attacker install a service worker on my site?

Only by running script on your origin, and that's why XSS is more dangerous in a PWA. Browsers require the worker script to be same-origin, served over HTTPS with a JavaScript MIME type, without redirects, and they limit its scope to the script's directory unless the Service-Worker-Allowed header says otherwise. But an attacker with XSS can register any same-origin JavaScript URL they can influence (a JSONP endpoint, an uploaded file served as JavaScript) as a worker, or write poisoned entries into Cache Storage that your worker later serves. Defenses: a strict Content Security Policy, rejecting requests with the Service-Worker: script header on every URL except your real worker, never serving user uploads from your app's origin, and integrity checks on cached app shell files. Service Worker Security covers each.

How do I remove a broken service worker from users' browsers?

Deploy a fix at the same URL as the broken worker. The browser's update check for the worker script is never intercepted by a service worker, so the next in-scope navigation fetches it (as does a functional event such as push, if the last check was more than 24 hours ago). Since Chrome 68 the check bypasses the HTTP cache by default (updateViaCache: "imports"), so what can still serve a stale sw.js is your CDN, or the HTTP cache if you registered with updateViaCache: "all". For a full reset, ship a kill-switch worker that calls skipWaiting(), deletes its caches, unregisters itself in activate and tells its clients to reload. Alternatively, send Clear-Site-Data: "storage" on the worker script response, which unregisters every worker for the origin and clears its storage (it's ignored on responses a worker fabricates). Never delete sw.js or return 404 for it without a replacement plan. Pitfalls & Anti-Patterns and Updating Service Workers include tested kill-switch code.

Is it safe to cache authenticated or personal responses?

Only deliberately, and with cleanup. Cache Storage ignores Cache-Control, so a response marked private or no-store is stored if your worker puts it there, and it stays after the user signs out. On a shared computer, the next user of the same browser profile could then see it offline. Rules to follow: cache personal data only when offline access is a real feature; keep it in named caches or IndexedDB stores you can delete; clear them on sign-out (and use Clear-Site-Data on the sign-out response); never cache responses to requests that carry an Authorization header by default; and never attach credentials to requests the worker didn't expect, since navigations from other sites also pass through it. Service Worker Security and Privacy & Storage Partitioning explain the details.

What permissions can a PWA ask for, and does installing grant more?

A PWA uses the same permission model as any website: notifications (which also cover push), geolocation, camera and microphone, clipboard, persistent storage, and on Chromium, APIs such as Bluetooth, USB, HID, serial, file system access and window management (Web Serial is no longer Chromium-only: Firefox 151 on desktop ships it, gated behind a site-permission add-on). Each prompt needs a secure context and usually a user gesture. Installing grants almost nothing extra by itself, but it changes some behavior: on iOS, push and badging exist only in Home Screen web apps; Chromium grants persist() more readily to installed apps; Periodic Background Sync requires installation; and desktop OS integration such as file handlers applies only to installed apps. Ask for permissions in context, after explaining why, because a denied permission can only be reset by the user. Use navigator.permissions.query() to check state without prompting. See Permissions.

Further reading

On this site

External references