How Progressive Web Apps Work

Photo: Godfrey Atima / Pexels
A progressive web app (PWA) is a website that can be installed on a phone or desktop, open in its own window, keep working when the network drops, and send push notifications, all without an app store. When Twitter made its PWA, Twitter Lite, the default mobile web experience in April 2017, it measured about 600 KB over the wire against 23.5 MB for the native Android app, and a 30% drop in average load time for logged-in users (Google, web.dev case study, 2017). Two browser features make this possible: a JSON manifest that describes the app, and a service worker that sits between the page and the network.
This article explains what those two pieces do, how a service worker's lifecycle and caching strategies work, which rules decide whether a browser offers installation, and where PWAs still fall short of native apps. The numbers come from Google's published Twitter case study and from browser vendors' documentation.
What a progressive web app actually is
A progressive web app is a website built with a web app manifest and a service worker so it can be installed, cached, and used offline like a native app. It is not a separate technology stack. It is an ordinary website that opts into a small set of browser APIs defined in open specifications, not owned by one vendor.
The term comes from a post titled "Progressive Web Apps: Escaping Tabs Without Losing Our Soul," published by Google Chrome engineer Alex Russell on June 15, 2015. Russell listed the qualities of a progressive app: responsive, connectivity independent, app-like in its interactions, fresh, safe (served over TLS), discoverable, re-engageable, installable, and linkable. Notice that these are properties of a good website, not of a special file format. The word "progressive" means the extra abilities are layered on where the browser supports them, and the site still works as a normal page where it does not.
That framing explains why PWAs rarely require a rewrite in another language. The enhancement sits on top of HTML, CSS, and JavaScript the team already has. If you want the fundamentals first, our primer on HTML, CSS, and JavaScript fundamentals covers the layer a PWA builds on.
A PWA versus a responsive website
A responsive site resizes to fit any screen but still behaves like a browser tab: it needs a connection, has no icon of its own, and cannot receive background messages. A PWA can launch from a home-screen icon in a standalone window, keep serving pages when the connection fails, and receive messages when no tab is open. A site can be fully responsive and still not be a PWA if it never registers a service worker. For the layout side of that story, see responsive design and mobile-first development.
The three ingredients
Installability rests on three things:
- HTTPS. Service workers run only in secure contexts. The exception is
localhost, which browsers treat as secure for development. - A web app manifest. A JSON file that tells the browser the app's name, icons, start page, and display mode.
- A service worker. A script that handles network requests for the site.
The third item deserves a caveat that older PWA tutorials get wrong. Chrome's documentation says it removed the service worker requirement for installing from the browser menu: version 108 on mobile and version 112 on desktop. The automatic install prompt, however, still requires a fetch() handler, according to the same Chrome post (last updated December 5, 2023). Chrome made the change because sites were adding empty fetch handlers just to qualify, which defeated the purpose of the check.
The web app manifest: an identity card for your site
A web app manifest is a JSON file linked from a page's HTML with <link rel="manifest" href="manifest.webmanifest">. It supplies the metadata a browser or operating system needs to treat a page as an installable application. A minimal one, adapted from MDN's example, looks like this:
{
"short_name": "MDN",
"name": "MDN Web Docs",
"icons": [
{ "src": "/favicon-192x192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/favicon-512x512.png", "sizes": "512x512", "type": "image/png" }
],
"start_url": ".",
"display": "standalone",
"theme_color": "black",
"background_color": "white"
}
Each member has a job:
| Member | What it controls |
|---|---|
name, short_name |
The full name for install prompts and splash screens, and a shorter label for the home screen |
start_url |
The page that opens when the icon is tapped |
display |
standalone hides the address bar and tab strip; other values are fullscreen, minimal-ui, and browser |
icons |
Images at several sizes, including 192 px and 512 px, used for the icon and splash screen |
theme_color |
The color of operating-system chrome around the app, such as a title bar |
background_color |
The splash-screen background shown before the app paints |
scope |
Which URLs belong to the app; links outside the scope open in a regular browser |
id |
A stable identity so the browser can tell apps apart if start_url changes |
shortcuts |
Quick actions in the app's context menu |
What Chrome requires for an install prompt
Chrome's installability documentation (last updated September 19, 2024) lists the conditions for the beforeinstallprompt event. The app must not already be installed, and the user must have clicked or tapped the page at least once and spent at least 30 seconds on it. It must be served over HTTPS. And the manifest must include short_name or name, icons that include a 192 px and a 512 px image, a start_url, and a display value of fullscreen, standalone, minimal-ui, or window-controls-overlay. The prefer_related_applications field must be absent or false. If a manifest sets it to true along with related_applications, Android users are sent to Google Play to install the native app instead.
The 30-second engagement rule is a quiet but important detail. A brand-new site cannot trigger the prompt on the first second of the first visit, so developers who test installation in a hurry sometimes conclude the manifest is broken when it is not.
What the manifest does not do
The manifest provides no offline behavior. It controls installability and home-screen appearance. A site can have a flawless manifest, install cleanly, and still show an error page the moment the network drops, because offline behavior belongs to a separate API.
The service worker: where offline actually happens
A service worker is a JavaScript file that the browser runs in the background, separate from any page. According to MDN, it executes in a ServiceWorkerGlobalScope off the main thread, with no access to the DOM (the page's document structure). It acts as a programmable proxy: a fetch event fires for every request from pages it controls, including requests for resources those pages load, and the worker can answer from a cache, from the network, or by constructing its own response.
Picture a receptionist who screens every call to an office. Most calls get forwarded to the person who is supposed to take them (the network). But the receptionist can answer simple questions directly from a notepad (the cache), take a message if the person is out (queue the request), or tell the caller to try later (a fallback page). The page does not need to know which happened.
Registering and scoping a worker
Registration is one call from the page:
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js", { scope: "/" });
}
The feature check matters because browsers without support simply run the site online. The scope defaults to the directory containing the worker script, so a worker at /js/sw.js can by default control only URLs under /js/. Putting the file at the site root gives it the whole origin. A server can widen the scope with the Service-Worker-Allowed header, and registration fails if the scope is broader than the script's location allows without it.
The lifecycle: install, wait, activate
A service worker moves through a lifecycle that surprises developers who expect normal scripts. MDN documents it this way:
- Register. The browser downloads and registers the script.
- Install. An
installevent is always the first event a service worker receives. This is where you usually precache core files usingevent.waitUntil(), which holds installation open until the promise finishes. If it rejects, installation fails. - Waiting. If a previous version still controls open pages, the new version waits. It does not take over mid-session.
- Activate. Fires once the old version is retired. This is the right place to delete old caches.
- Idle and wake. The browser can stop an idle worker to save memory and restart it when an event arrives.
Two methods change the waiting behavior. skipWaiting() asks the new worker to activate without waiting for open pages to close, and clients.claim() lets an activated worker take control of pages that loaded without one. MDN explains why the second exists: "a document starts life with or without a service worker and maintains that for its lifetime," so a freshly installed worker does not control the page that registered it until the page is reloaded.
This design is deliberate. If an update could swap the worker in the middle of a session, a page might receive files from two incompatible versions at once. Waiting trades speed of rollout for consistency.
Why a worker cannot hold variables
Because the browser can terminate an idle worker at any time, global variables do not survive between events. Anything that must persist belongs in IndexedDB or the Cache API. MDN adds that localStorage is synchronous and not allowed in service workers. Treat each event handler as if the worker has just been born.
Caching strategies, in plain terms
The Cache API stores Request and Response pairs. MDN notes that items "do not get updated unless explicitly requested; they don't expire unless deleted," so your own code is responsible for refreshing and purging. The API also does not follow normal HTTP caching headers.
A strategy is the policy your fetch handler applies. Google's Workbox library packages the common ones, and its documentation lists five:
| Strategy | Behavior | Good for |
|---|---|---|
| Cache First | Serves from cache; falls back to network and stores the result | Fonts, logos, versioned static files |
| Network First | Tries the network; serves cache if the network fails | News feeds, dashboards, anything that should be fresh |
| Stale-While-Revalidate | Responds from cache at once, then updates the cache from the network | Avatars, article lists where slightly old data is fine |
| Network Only | Always uses the network | Payments, one-off actions |
| Cache Only | Uses only cached responses | Files you precached on your own |
Here is what a cache-first fetch handler looks like in practice, simplified from MDN's example:
self.addEventListener("fetch", (event) => {
event.respondWith((async () => {
const cached = await caches.match(event.request);
if (cached) return cached;
try {
const response = await fetch(event.request);
const cache = await caches.open("v1");
cache.put(event.request, response.clone());
return response;
} catch (error) {
return new Response("Network error", { status: 408 });
}
})());
});
The response.clone() call is not decoration. A response body is a stream that can be read only once, so the code stores a copy and returns the original.
Versioning caches
When you ship new assets, give the cache a new name, such as v2, so the old worker still finds its own v1 files until it retires. In the activate event, delete every cache not on your keep list. Skipping this step is the classic reason PWAs fill users' storage with stale files.
The offline fallback page
The simplest useful offline behavior is a fallback page. Precache a small offline.html during install, and in the fetch handler return it when a navigation request fails. Twitter followed the same path. According to the case study, it added service workers in stages: first a custom offline page, then offline caching of static assets such as CSS, images, and JavaScript bundles, then offline caching of the application shell.
The application shell is the minimum HTML, CSS, and JavaScript needed to draw the interface. Cache the shell and the app opens instantly even before any fresh data arrives. The data then fills in from the network or from a cached copy.
Navigation preload
A subtle cost of service workers is startup time: if the worker has been stopped, the browser must start it before it can fetch anything. MDN describes navigation preload as the fix. By calling self.registration.navigationPreload.enable() during activation, the browser starts the network request in parallel with worker startup, and the fetch handler reads it from event.preloadResponse.
Background capabilities: push and sync
Because a service worker can run when no tab is open, it can do two things ordinary page scripts cannot.
Push notifications. The service worker receives push messages from the browser's push service and shows a notification, even when the site is closed. Twitter reported sending more than 10 million push notifications per day through Twitter Lite in its case study.
Background sync. The Background Synchronization API lets an app defer work until the device has a stable connection. An email client might let a user write and "send" messages offline; the service worker then sends them when connectivity returns. MDN labels this API "Limited availability," meaning it does not work in some widely used browsers, so treat it as an enhancement and check MDN's compatibility table before depending on it.
// In the page
const registration = await navigator.serviceWorker.ready;
await registration.sync.register("sync-messages");
// In the service worker
self.addEventListener("sync", (event) => {
if (event.tag === "sync-messages") {
event.waitUntil(sendOutboxMessages());
}
});
What the Twitter Lite numbers actually show
Twitter's case study is the best-documented public example, and it is worth reading closely because it shows PWA gains came from many techniques, not one.
| Metric (Twitter Lite, 2017) | Reported result |
|---|---|
| Size over the wire | About 600 KB vs 23.5 MB for the native Android app |
| Device storage | Less than 3% of Twitter for Android |
| First load over 3G | Under 5 seconds on most devices |
| Returning-user boot | Under 3 seconds |
| Average load time, logged-in users | Down 30% |
| 99th-percentile time-to-interactive latency | Down 50% |
| Pages per session | Up 65% |
| Tweets sent | Up 75% |
| Bounce rate | Down 20% |
| Data used scrolling timelines | Cut by as much as 70% through image optimization |
| Home-screen launches | 250,000 unique daily users, about 4 launches a day each |
Two cautions apply. First, the service worker was only part of the engineering. Twitter also streamed the initial HTML, preloaded critical resources, split code into granular chunks so the first load fetched only what the visible screen needed, and followed parts of the PRPL pattern. Second, the figures are the company's own, published once, and the case study does not isolate how much each technique contributed. The numbers describe a rebuild, not a controlled experiment on service workers alone.
The performance lesson generalizes, though. Faster pages tend to reduce abandonment, which is why our explainers on web performance optimization and why web performance affects SEO overlap with PWA work. A CDN also helps first loads on slow networks, and how CDN caching speeds up websites explains that layer.
How a PWA affects rendering and loading
A service worker does not change how the browser parses HTML or builds the page, but it can change where the bytes come from. If you want the browser-side picture, read how browsers render web pages. And for how server-rendered HTML fits with a client-side app shell, see what server-side rendering solves. The two approaches combine well: server-rendered HTML for the first view, a service worker for every later one.
Platform differences you have to plan around
PWAs run on every modern browser engine, but not equally.
| Capability | Chrome and Edge (Android, desktop) | Safari on iOS and iPadOS |
|---|---|---|
| Install from the browser | Yes, including a prompt via beforeinstallprompt |
Via "Add to Home Screen" |
| Web Push | Yes | Since iOS and iPadOS 16.4, for web apps added to the Home Screen only |
| Background Sync | MDN lists it as limited availability; check its compatibility table | MDN lists it as limited availability; check its compatibility table |
| Service workers and Cache API | Yes | Yes |
Apple announced Web Push for iOS and iPadOS on February 16, 2023, with 16.4 beta 1, in a WebKit blog post. The rules are specific: Web Push works only for web apps added to the Home Screen, the site must serve a manifest with display set to standalone or fullscreen, and the app must request permission in response to a direct user action such as tapping a "subscribe" button. It uses the standard Push API, Notifications API, and Service Workers together, and the Badging API is also supported. No Apple Developer Program membership is needed.
Storage behaves differently too. MDN's page on storage quotas notes that Safari, with cross-site tracking prevention on, deletes an origin's script-created data if the user has not interacted with it in seven days of browser use, with server-set cookies exempt. For a rarely visited site, that means a cached offline experience can vanish without warning. Chromium and Firefox use least-recently-used eviction under storage pressure instead. MDN also says developers can request persistent storage with navigator.storage.persist(), and that Firefox shows a permission popup while Safari and Chromium browsers decide based on how the user has interacted with the site.
A PWA's reach depends on platform vendors' policies as much as on web standards, so check each platform's current documentation before committing to a feature.
Where PWAs beat native apps, and where they lose
PWAs win when reach, update speed, and install friction matter more than deep device access.
- Poor or intermittent connectivity. A service worker cache keeps core screens usable. Twitter Lite's first load under five seconds over 3G was a design goal for markets with slow networks.
- Install friction. A PWA installs from a link, with no store review and no multi-megabyte download. Twitter reported 250,000 unique daily users launching it from the home screen.
- One codebase. The same code runs on Android, desktop, and iOS, though with different capabilities.
- Instant updates. The next visit fetches the new service worker, and the lifecycle rules above decide when it takes over. There is no update to approve.
PWAs lose where the browser sandbox restricts capability. Browsers expose far fewer hardware and operating-system features than native SDKs, with the exact set varying by platform and changing over time, so apps that need deep Bluetooth control, continuous background location, or demanding camera pipelines usually need native code. The reverse is also true: native apps need separate builds per platform and store review for every update.
| Dimension | Native app | Progressive web app | Traditional website |
|---|---|---|---|
| Installation | App store download | Browser install, no store review | Not installable |
| Offline | Built into the binary | Only what the service worker cached | None |
| Push notifications | Full platform support | Supported, with the iOS conditions above | Not supported |
| Distribution | Store review and fees | A URL | A URL |
| Updates | User or store gated | Service worker lifecycle | Instant |
| Hardware access | Broad | Limited to browser APIs | Limited to browser APIs |
Common mistakes and misconceptions
"A manifest makes my site work offline"
It does not. Offline support requires a service worker with an explicit caching strategy. A manifest alone gives you an icon.
"Caching everything is safer"
Caching everything is how stale data and bloated storage happen. Choose per resource: cache-first for versioned static files, network-first for personalized or time-sensitive responses. Never cache authenticated API responses in a shared way without thinking about who the next user of the device might be.
"A service worker updates immediately"
It updates according to the lifecycle: the new worker installs in the background, then waits until no open pages use the old one. Developers who forget this ship a fix and keep seeing the old bug, because their own tab is still controlled by the old worker. Closing all tabs, or calling skipWaiting() deliberately, resolves it.
"PWAs behave the same everywhere"
Support for push, background sync, and storage persistence differs between browsers and changes over time. Test on the platforms your users have, and check MDN's compatibility tables instead of assuming parity.
"A PWA is a lower-quality app"
Quality depends on the engineering. A PWA with a carefully designed app shell and cache policy can load faster than a poorly built native app. The format limits some capabilities, not the polish of what is built.
Frequently asked questions
Q: What is the difference between a manifest file and a service worker? A: The manifest is a JSON file naming the app, its icons, start URL, and display mode, and it makes the site installable. The service worker is a background script that intercepts network requests, enabling caching, push, and sync. Neither substitutes for the other.
Q: Do PWAs work without an internet connection? A: Only for content the service worker has already cached, using a strategy written into its fetch handler. A page the user never visited while online still needs the network, so offline coverage is only as good as the caching code behind it.
Q: Why do service workers require HTTPS?
A: A service worker can intercept and rewrite every request a page makes, so running it over an unencrypted connection would let an attacker hijack that control. MDN states that service workers are restricted to secure contexts, with localhost allowed for development.
Q: Do PWAs work the same on iPhone as on Android? A: Not entirely. Apple added Web Push in iOS and iPadOS 16.4, and only for web apps added to the Home Screen with a manifest whose display mode is standalone or fullscreen. Safari also deletes script-created storage after seven days without user interaction when tracking prevention is on.
Q: Does Chrome still require a service worker to install a PWA? A: Not for installing from the browser menu: Chrome removed that requirement in version 108 on mobile and 112 on desktop. The automatic install prompt still requires a fetch handler, according to Chrome's blog post on installability criteria (updated December 5, 2023).
Q: How big can a PWA's offline cache be? A: It depends on the browser and the device's free disk space. MDN says Chrome allows an origin up to 60% of total disk size, Firefox the smaller of 10% of total disk size or 10 GiB in best-effort mode, and Safari around 60% of total disk for browser apps. Browsers can evict an origin's data under storage pressure.
Q: What companies have shipped a PWA? A: Twitter is the best-documented case: Twitter Lite became the default mobile web experience in April 2017 and reported a 65% rise in pages per session and a 75% rise in tweets sent. Figures like these are company-reported and reflect a full rebuild.
The takeaway
A progressive web app is a normal website plus two small files that change what the browser is willing to do for it. The manifest gives the site an identity the operating system can install. The service worker gives it a programmable layer between the page and the network, with a lifecycle designed so that an update never changes the code under a running page.
Everything people like about PWAs, instant repeat loads, offline screens, and installable icons, comes from the service worker's cache policy, and none of it is automatic. The gains in Twitter's case study came from a stack of techniques working together, and the platform gaps, especially around iOS, are real constraints on product planning. Build the offline behavior deliberately, test it on the platforms you care about, and treat the manifest as the easy part.
Sources
- Google, Twitter Lite and high retention with web push notifications and a PWA case study, web.dev (2017).
- Alex Russell, Progressive Web Apps: Escaping Tabs Without Losing Our Soul (June 15, 2015).
- MDN Web Docs, Using Service Workers.
- MDN Web Docs, Cache API and Storage quotas and eviction criteria.
- MDN Web Docs, Background Synchronization API and Web app manifest.
- Chrome for Developers, Installation criteria and Revisiting Chrome's installability criteria.
- Chrome for Developers, Workbox strategies.
- WebKit, Web Push for Web Apps on iOS and iPadOS (February 16, 2023).
Read next:
Follow new articles
Add SciHub101 to your feed reader to see new science and technology stories.
About the Author
SciHub101 is an independent science and technology publication founded and edited by Buğra Sözeri, a software engineer and app developer based in Türkiye. AI tools assist with drafting and research. Source documentation in the existing archive is uneven, and articles are being reviewed and corrected. The Editorial Team byline identifies editorial responsibility; it does not certify specialist or independent human review. Where we compare products, anything built by our own team is labelled as such — see the editorial guidelines and About page for how corrections and disclosures work.





