Skip to main content
Blog

Progressive Web Apps: The Best of Web and Mobile

Last updated Mobile

A progressive web app is a website that behaves like an installed application: it loads instantly on a bad connection, works when the network is gone, can be launched from the home screen without a browser frame, and can receive notifications. No store, no review queue, no separate codebase — the same URL that anyone can visit is also the app.

The idea has been available for years and is still under-used, partly because early attempts over-promised and partly because the interesting parts — service workers and caching strategy — are genuinely subtle. This guide covers what a PWA is made of, how each piece works, which caching strategy to use where, what the platform limits actually are today, and when a PWA is the right answer rather than a compromise.


What you will learn
  • The three technical requirements that make a web app "progressive"
  • How a service worker works, and the lifecycle that trips everyone up
  • Five caching strategies and exactly when to use each
  • Offline design, background sync and handling stale data honestly
  • Installation, push notifications and platform differences that matter
  • When to choose a PWA, and when not to
In this article
  1. What makes an app "progressive"
  2. The three required pieces
  3. Service workers explained properly
  4. The lifecycle, and why your update did not appear
  5. Caching strategies, and choosing between them
  6. Designing for offline
  7. Background sync and deferred work
  8. Installation and the app shell
  9. Push notifications
  10. Storage, quotas and eviction
  11. Performance and the metrics that matter
  12. Platform limits and honest trade-offs
  13. Ten mistakes
  14. A worked example: taking an existing site offline-capable
  15. Frequently asked questions

1. What makes an app "progressive"

The word "progressive" comes from progressive enhancement: the app works for everyone, and gets better where the browser supports more. Someone on an old browser gets a normal website. Someone on a modern one gets offline support, an install prompt and notifications. There is no separate build and no feature detection cliff — capability is layered on.

The practical definition is behavioural rather than technical. A PWA is a web application that is:

  • Reliable — it loads instantly, even offline or on a hostile connection, because the shell is served from a local cache rather than the network.
  • Installable — it can be added to the home screen and launched in its own window without browser chrome.
  • Capable — it can use device features the browser exposes: camera, geolocation, notifications, file access, background work.

What it is not: a native app. The distinction matters for setting expectations, and section 12 covers exactly where the ceiling is.

2. The three required pieces

PieceWhat it doesWithout it
HTTPSSecure origin — service workers can intercept every request, so the connection must be trustworthyService workers refuse to register at all
Web app manifestJSON describing name, icons, colours, start URL and display modeNo install prompt, no standalone window
Service workerA background script that intercepts network requests and can answer from cacheNo offline support, no push, no background sync

The manifest is the easy part and is worth getting exactly right, because it controls what installation actually produces:

Manifest fieldWhat it controls
name and short nameThe full title, and the shorter label shown under the home-screen icon where space is tight
start URLWhere the app opens when launched. Adding a marker parameter here lets analytics separate installed traffic from browser traffic — a small change that tells you whether installation is worth further investment
scopeWhich URLs belong to the app; navigating outside it hands the user back to the browser
displayStandalone removes the address bar entirely, which is what makes an installed app feel like an app
background and theme colourThe splash screen colour on launch, and the tint applied to system interface elements
iconsAt minimum a medium and a large square icon, plus a maskable variant

The maskable icon is the detail most teams miss. Without one, platforms that apply their own icon shape will crop your square icon awkwardly or drop it inside a white circle. Design the maskable version with the important content inside the safe zone at the centre, and check it against both circular and rounded-square masks before shipping.

3. Service workers explained properly

A service worker is a JavaScript file that runs separately from your page, in its own worker context, with no access to the DOM. Its defining ability is that it sits between your application and the network: every request the page makes passes through it first, and the worker decides whether to fetch it, serve it from cache, or synthesise a response entirely.

Three consequences follow, and they explain most of its behaviour:

  • It runs when the page does not. This is how push notifications and background sync work — the browser can wake the worker without an open tab.
  • It is event-driven and short-lived. The browser starts it, delivers an event, and stops it. You cannot keep state in a variable between events; use a storage API instead.
  • It is a proxy you wrote. This is powerful and dangerous in equal measure: a bug in the fetch handler can make your entire site unreachable until the worker is replaced.

A minimal but complete worker handles three events:

A complete worker is short. On install it opens a versioned cache and stores the shell — the root page, stylesheet, script bundle and a dedicated offline page — then optionally signals that it should take over immediately. On activate it lists every cache the origin holds, deletes any whose name does not match the current version, and claims control of open pages. On fetch it inspects the request: navigation requests go to the network first and fall back to the offline page when that fails, while other request types follow whichever strategy suits them.

Three details in that description carry most of the weight. The cache name contains a version, which is what makes cleanup possible at all. Deletion happens on activate rather than install, so the previous cache stays intact until the new worker is genuinely ready to serve. And the fetch handler distinguishes request types rather than applying one rule to everything, which is the difference between a working app and one that serves stale HTML forever.

4. The lifecycle, and why your update did not appear

The single most common source of confusion is that a new service worker does not take control immediately. The lifecycle has four states:

  1. Installing — the new worker runs its install handler, typically pre-caching the shell.
  2. Waiting — installation succeeded, but an old worker is still controlling open pages. The new one waits.
  3. Active — the old worker is gone and the new one is in charge.
  4. Redundant — replaced or failed.

The waiting state exists for a good reason: swapping the worker under a running page can leave that page mixing old and new assets. But it means a user who never fully closes your app can run an old version for days. Every tab must be closed — reloading is not enough, because a reload keeps the old worker alive during the transition.

You have two honest options. Call skipWaiting() to take over immediately, accepting that an open page may receive assets from a newer version than it was loaded with — safe if your assets are versioned and additive. Or detect the waiting worker and show the user a small "A new version is available — reload" prompt, which is the better experience for apps holding unsaved state.

The escape hatch you must build first

Before shipping any service worker, ship the ability to remove it. A worker with a broken fetch handler can render your site permanently unloadable for users who already have it, and no amount of deploying fixed HTML helps because the worker is intercepting the request for that HTML. Keep a tested kill switch — a worker version whose install handler unregisters itself and clears all caches — and make sure you can deploy it independently.

5. Caching strategies, and choosing between them

A caching strategy is simply the rule your fetch handler applies. There are five worth knowing, and using the wrong one is the most common cause of "why is my app showing old data".

StrategyBehaviourUse forRisk
Cache firstServe from cache; only fetch if missingVersioned static assets, fonts, iconsStale forever if the URL never changes
Network firstTry network; fall back to cacheHTML pages, frequently changing dataSlow on poor connections until timeout
Stale while revalidateServe cache immediately, update in backgroundAvatars, feeds, non-critical contentUser sees one-version-old content
Network onlyNever cachePayments, authentication, anything transactionalFails offline, correctly
Cache onlyNever fetchPre-cached shell in a fully offline appRequires deliberate cache management

The practical mapping for most applications: cache first for hashed asset filenames, network first for navigation requests, stale-while-revalidate for API reads that tolerate slight staleness, and network only for anything that moves money or changes authentication. Decide per route rather than globally — a single strategy applied to everything is always wrong somewhere.

Add a timeout to network-first, otherwise a connection that is technically alive but effectively dead — the classic hotel wifi — hangs until the browser gives up. Racing the network against a three-second timer and falling back to cache converts a frustrating wait into an instant, slightly stale response.

6. Designing for offline

Offline is not a binary. There are four states a user can be in, and a good app distinguishes them:

  • Online and fast. Everything works.
  • Online but slow. The hardest state, because nothing has failed — it is just not arriving. Timeouts and cached fallbacks matter more here than in true offline.
  • Offline with cached data. Show it, and say clearly when it was last updated.
  • Offline with nothing cached. Show a useful offline page that explains what is available, not a browser error.

Three design rules cover most of it. Never lie about freshness — a timestamp saying "updated 3 hours ago" costs one line and prevents a whole class of user confusion. Never lose user input — persist form state locally as it is typed, so a lost connection does not discard ten minutes of work. And disable rather than fail — an action that cannot work offline should be visibly unavailable with an explanation, not a button that produces an error.

7. Background sync and deferred work

The most valuable offline capability is accepting a write while disconnected and completing it later. The pattern has three parts:

  1. Write locally first. Store the intended action in a local database with a client-generated identifier, and update the interface immediately so the user sees their action succeed.
  2. Queue it. Register a background sync so the browser can wake your service worker when connectivity returns — even if the app is closed.
  3. Reconcile. When the sync fires, send the queued items, handle conflicts, and update local state with the server's response.

Two details make this reliable. The client-generated identifier must be sent with the request so the server can recognise a duplicate — retries are certain, and without idempotency you create the same record twice. And the queue needs a visible state in the interface: pending, syncing, failed. Silent queues that quietly drop items destroy trust in a way that a visible error never does.

Background sync support is uneven across browsers, so treat it as an enhancement: always also attempt to flush the queue when the app next opens and connectivity is present.

8. Installation and the app shell

Installation is what turns a bookmark into something that feels like an app. Browsers decide when to offer it based on criteria that include having a valid manifest, a registered service worker, and some evidence of user engagement.

Do not rely on the browser's own prompt alone. Capture the install event, suppress the default banner, and offer installation at a moment when the user has just experienced value — after completing a task, not on first load. An install prompt shown in the first three seconds is dismissed reflexively and, on some platforms, cannot be shown again for a long time.

The app shell pattern is what makes an installed PWA feel instant. Separate the static frame — header, navigation, layout skeleton — from the content that fills it. Pre-cache the shell during service worker installation so it renders from local storage in milliseconds, then populate it with data from cache or network. The result is that launching the app never shows a blank screen, even on the first offline launch.

9. Push notifications

Push works through three parties: your server sends a message to a push service run by the browser vendor, which delivers it to the device, which wakes your service worker to display it.

The engineering is straightforward. The discipline is not. Permission prompts shown without context are refused overwhelmingly, and on most platforms a refusal is effectively permanent — the user must dig into settings to reverse it. Ask only after the user has done something implying they want updates, explain what you will send, and provide a way to change it later.

The sequence is: request permission at a moment of intent, subscribe through the service worker registration using your server's public signing key, then send the resulting subscription object to your backend to store against that user. Browsers require that every push results in a visible notification — a deliberate rule that prevents silent background tracking, and one worth knowing before you design a feature that assumes otherwise.

Plan for subscriptions expiring, and remove any that your push service reports as dead. Without that cleanup, your send volume slowly fills with addresses nobody is listening to, delivery statistics become meaningless, and you lose the ability to tell a delivery problem from an engagement problem.

10. Storage, quotas and eviction

PWAs have several storage mechanisms with different characteristics, and choosing wrongly causes problems that only appear at scale.

MechanismGood forLimits
Cache StorageHTTP responses — assets and API resultsPart of the origin quota
IndexedDBStructured application data, queuesPart of the origin quota; asynchronous
localStorageSmall preferences onlyA few megabytes, synchronous — blocks the main thread
CookiesServer-read session stateTiny, sent with every request

Quota is granted per origin as a share of available disk and is not guaranteed. Under storage pressure browsers evict data from origins the user engages with least, and eviction is typically all-or-nothing for an origin. If your app holds data that cannot be regenerated from a server, request persistent storage — a request that browsers grant based on engagement signals such as installation. Even then, treat local data as a cache with a durable copy elsewhere unless the user has explicitly saved something locally.

11. Performance and the metrics that matter

A service worker does not make a slow app fast; it makes a fast app instant on repeat visits. First-visit performance is still ordinary web performance work.

MetricTargetMain lever
Largest contentful paintUnder 2.5sServer response time, image weight, critical CSS
Interaction to next paintUnder 200msBreaking up long JavaScript tasks
Cumulative layout shiftUnder 0.1Explicit dimensions on images and embeds
Repeat-visit loadUnder 1sPre-cached app shell
Offline launchAlways renders somethingShell plus a real offline page

Measure with real user data, not only lab tests. Segment by whether the session was launched from the home screen — installed users behave differently and are usually your most engaged cohort, which makes their experience worth optimising specifically.

12. Platform limits and honest trade-offs

Where a PWA genuinely cannot compete with a native app:

  • Background execution is limited. Periodic background work is restricted or unsupported depending on platform, so apps needing continuous background activity — fitness tracking, real-time location logging — are poor fits.
  • Deep hardware and OS integration is uneven. Bluetooth, NFC, widgets, watch companions, system-wide sharing targets and background audio behave inconsistently across platforms.
  • Notification behaviour varies. Support and reliability differ meaningfully between platforms, and rich notification features are not uniformly available.
  • Storage can be evicted. A native app's files persist until uninstalled; a PWA's storage is subject to browser policy.
  • Discovery is different. No store listing means no store search traffic, and installation requires a user who is already on your site.

Where a PWA is clearly better: instant updates with no review cycle, one codebase and one URL, no store commission, linkable state so any screen can be shared, and zero installation friction for first-time users. For content sites, tools, internal applications, booking and ordering flows, and anything where most users arrive from search or a link, those advantages usually dominate.

13. Ten mistakes

  1. Shipping a service worker without an unregister path. The one bug that can make a site permanently unreachable.
  2. Cache-first on HTML. Users see an old page indefinitely and cannot understand why.
  3. Never invalidating caches. Version your cache name and delete old versions on activate.
  4. Asking for notification permission on first load. Refused reflexively, and often unrecoverable.
  5. Network-first without a timeout. A weak connection is worse than no connection.
  6. Assuming background sync exists. Support is uneven; always also flush the queue on next launch.
  7. Caching authenticated responses in a shared cache. One user can be served another's data.
  8. No maskable icon. Your app looks amateur on the home screen, which is the one place it must not.
  9. Treating local storage as durable. It can be evicted; keep a server copy of anything that matters.
  10. Hiding staleness. Showing cached data without saying when it was fetched turns a feature into a bug report.

14. A worked example: taking an existing site offline-capable

Consider a field-reporting tool used by inspectors who spend their day in basements, warehouses and lift shafts. It is an ordinary server-rendered web application that works well on a desk and fails constantly in the field. Making it a PWA takes four passes, each shippable on its own.

Pass one: the shell. Add a manifest with proper icons including a maskable variant, register a service worker, and pre-cache the layout, stylesheet, script bundle and an offline page. Serve navigation requests network-first with a three-second timeout. Nothing about the application logic changes. The effect is immediate and visible: repeat loads become instant, and a lost connection produces a branded page explaining the situation instead of a browser error. This is a day of work and it is where most of the perceived quality improvement comes from.

Pass two: read offline. The inspector's assigned jobs for the day are fetched on login and stored in a local database, along with the reference material each job needs. The job list is served stale-while-revalidate, so it appears instantly and updates quietly when connectivity allows. Every screen showing cached data carries a small line saying when it was last refreshed. Now the tool is usable in a basement, which is the point at which people start relying on it.

Pass three: write offline. Completing a report writes to local storage first with a client-generated identifier, updates the interface immediately, and enqueues the submission. A background sync registration flushes the queue when connectivity returns, and the app also attempts a flush on every launch, because background sync cannot be relied on everywhere. The queue is visible: a small indicator shows how many reports are waiting, and tapping it lists them with their state. Photographs attached to reports are stored as blobs and uploaded separately, so a large image on a weak connection does not block the report itself.

Pass four: the details that decide whether people trust it. Conflict handling when a job is reassigned while an inspector is offline. A clear failure state for reports the server rejected, with the reason and a way to correct them. Storage pressure handling — old completed reports are purged locally once confirmed by the server. And a persistent storage request made after the app is installed, so the browser is less likely to evict data an inspector has not yet uploaded.

The pattern generalises well beyond field tools. Each pass is independently valuable, the risky work is concentrated in pass three, and the first pass — the cheapest — delivers a disproportionate share of the perceived benefit.

15. Frequently asked questions

Can a PWA replace our native app?

It depends entirely on what the app does. For content, commerce, booking, dashboards and most internal tools, yes — and you gain instant updates and one codebase. For apps depending on continuous background activity, deep hardware integration, or store discovery as a primary acquisition channel, no. A common and sensible pattern is a PWA as the primary experience with a native app for the subset of users who need the extra capability.

Do PWAs work on iOS?

Yes, and support has improved considerably, including web push for installed apps. Differences remain in installation flow, some storage behaviour and certain capabilities, so test on the actual platform rather than assuming parity. Design so the core experience does not depend on the most advanced capabilities, and treat those as enhancements — which is what "progressive" meant in the first place.

How much work is it to add PWA capability to an existing site?

A basic version — manifest, icons, a service worker caching the shell and serving an offline page — is typically a day or two on a site that is already reasonably fast. Meaningful offline functionality with queued writes is a genuine project measured in weeks, because it introduces local persistence, sync and conflict handling. Start with the cheap version, measure whether installed users behave differently, and invest further based on that.

Should I use a service worker library or write my own?

Use a library for anything beyond a trivial worker. The lifecycle, cache versioning, route matching and expiry policies contain many small correctness details that established libraries have already got right. Write your own only when you have unusual requirements, and even then write it after reading how the libraries handle the edge cases.

How do I test offline behaviour reliably?

Browser developer tools can simulate offline and throttled connections, which covers most cases. Also test the harder scenarios manually: an actively bad connection rather than a disconnected one, an app left open across a deployment, and a first launch from the home screen with no network at all. Automated end-to-end tests with the network intercepted are valuable for the critical journeys, especially the queue-and-sync path.

Does a PWA help or hurt SEO?

It helps, provided pages are server-rendered and each view has a real URL. Speed is a ranking factor and PWAs are typically fast. The failure case is a single-page application where all content is client-rendered and no meaningful HTML is returned — that is a rendering problem rather than a PWA problem, but it is common enough in the same codebases that it is worth checking explicitly.

Can we charge for a PWA?

Yes, using ordinary web payments, and without the commission a store would take. What you lose is store-managed subscriptions, family sharing and the trust some users place in store billing. For business and professional tools this is usually a clear win; for consumer applications competing on impulse purchases, store presence carries weight that is hard to replicate.

What is the minimum viable PWA?

HTTPS, a correct manifest with a maskable icon, and a service worker that pre-caches the app shell, serves navigation requests network-first with a timeout, and shows a genuinely useful offline page. That is a day of work, delivers instant repeat loads and a real home-screen presence, and gives you data on whether deeper investment is justified.

Key takeaways

  • Three pieces make a PWA: HTTPS, a manifest, and a service worker. Everything else is refinement.
  • Choose caching strategy per route. Cache-first for hashed assets, network-first with a timeout for pages, network-only for money.
  • Build the kill switch first. A broken worker can make your site unreachable, and only another worker can fix it.
  • Offline is four states, not two. The slow-but-connected case is the one that needs the most design.
  • Never hide staleness or lose input. Timestamps and locally persisted forms prevent most offline frustration.
  • Ask for permissions after value, not before. A refused notification prompt is usually unrecoverable.

The best argument for a PWA is not the technology. It is that the thing a user shares, the thing search engines index, and the thing sitting on their home screen are all the same artefact — one deployment, one codebase, no gatekeeper between a fix and the people who need it.

Enjoyed this article?

Get more engineering insights from ELIVTECH — or talk to us about your project.

Get in touch