Key Takeaway: Progressive Web Apps combine the reach of the web with the experience of native apps. This guide covers PWA architecture, service workers, the web app manifest, offline caching strategies, performance budgets, and deployment pipelines. You will learn how to transform an existing web application into an installable, offline-capable PWA that meets Google’s installability criteria, scores above 90 on Lighthouse, and delivers sub-second loads on 3G connections. Every code pattern here is production-tested and framework-agnostic.
Overview
What Is a Progressive Web App?
A Progressive Web App (PWA) is a web application that uses modern web APIs (service workers, the web app manifest, and push notifications) to deliver a native-app-like experience on desktop and mobile. Unlike traditional websites, PWAs can be installed on the device, work offline or on unreliable networks, send push notifications, and access platform features previously reserved for native apps.
The term was coined by Google engineers Alex Russell and Frances Berriman in 2015 to describe applications that meet a specific set of quality criteria: responsive design, connectivity independence, app-like interactions, freshness, safety, discoverability, re-engageable, and installable. Today, every major browser engine supports the core PWA technologies, and both Android and desktop platforms allow installation directly from the browser without an app store.
3.5x
Higher engagement vs. mobile web (Google case data)
68%
Increase in traffic after PWA launch (industry median)
<1s
Target first contentful paint on 3G
90+
Lighthouse PWA score threshold
PWAs are not a single framework or library. They are a set of progressive enhancements layered on top of a standard web application. This means you can adopt PWA features incrementally, start with a manifest and basic service worker, then add offline support, push notifications, and background sync as your application requires them.
PWA Architecture
How a PWA Fits Together
PWA architecture has three foundational pillars: the web app manifest (a JSON file that declares app metadata), the service worker (a JavaScript file that runs in the background and intercepts network requests), and HTTPS (required for service worker registration and secure content delivery). Everything else (caching, push notifications, background sync) builds on these three.
Web App Manifest
A JSON file linked from the HTML head. Declares the app name, icons, theme color, display mode, and start URL. Without it, the browser cannot offer installation.
Service Worker
A background script that the browser installs and runs separately from the page. It intercepts fetch events, manages caches, and enables offline functionality and push notifications.
HTTPS Requirement
Service workers only register on secure origins. HTTPS prevents man-in-the-middle attacks that could hijack the worker and inject malicious cached responses.
Application Shell
The minimal HTML, CSS, and JavaScript needed to render the app’s UI chrome. Caching the app shell separately from content enables instant loading on repeat visits.
The request lifecycle in a PWA differs from a standard web app. When the browser navigates to a URL, the service worker intercepts the request via a fetch event handler. The worker checks its cache: if a valid cached response exists, it returns immediately; if not, it falls through to the network. This interception layer is what makes offline support and fine-grained caching control possible.
Key Insight, The service worker acts as a programmable network proxy between your web app and the network. Once installed, it controls all requests within its scope, even when the user is offline. This is powerful but requires careful cache management to avoid serving stale content.
Service Workers
Service Workers: The Offline Engine
The service worker is the most important, and most complex, component of a PWA. It has its own lifecycle: installation, activation, and a running state where it listens for events. Understanding this lifecycle is essential because it determines when your cached resources become available and when updates take effect.
Service Worker Lifecycle
| Phase | Trigger | What Happens | Your Responsibility |
|---|---|---|---|
| Registration | Page load | Browser downloads and parses the SW file | Call navigator.serviceWorker.register() after load |
| Installation | First registration or new SW version detected | install event fires; precache app shell |
Cache critical assets; call skipWaiting() if needed |
| Activation | Old SW releases control (no tabs open) or clients.claim() |
activate event fires; clean old caches |
Delete obsolete caches; call clients.claim() |
| Fetch Handling | Any request within scope | fetch event fires |
Implement caching strategy per request type |
| Update | Byte-for-byte change in SW file | New SW installs but waits to activate | Notify user; call skipWaiting() for immediate update |
Registration Code
Register the service worker from your main application script. Always check for support before calling register, and use a relative or absolute path that covers the scope you need:
if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js', { scope: '/' }).then(reg => { console.log('SW registered:', reg.scope); }).catch(err => { console.error('SW registration failed:', err); }); }); }
Performance Tip, Register the service worker after the load event, not on DOMContentLoaded. This ensures the SW download does not compete with critical rendering resources, keeping your first paint fast on initial visits.
Caching Strategies
Choosing the right caching strategy per resource type is the single most impactful decision in service worker design. Different resources have different freshness and reliability requirements:
| Strategy | Behavior | Best For | Trade-off |
|---|---|---|---|
| Cache First | Check cache, fall back to network | App shell, fonts, images | May serve stale content |
| Network First | Try network, fall back to cache | API data, dynamic content | Slower when offline-first is needed |
| Stale-While-Revalidate | Serve cache, update in background | Non-critical resources, frequent updates | Extra bandwidth for background updates |
| Network Only | Bypass cache entirely | Analytics, no-cache APIs | No offline support for that request |
| Cache Only | Serve only from cache | Pre-cached static assets | Never updates without SW change |
App Manifest
The Web App Manifest
The web app manifest is a JSON file that tells the browser how your PWA should behave when installed. Without a valid manifest, Chrome and Edge will not show the install prompt, and your app cannot appear in the OS app list or start menu. The manifest is linked from your HTML head with a <link rel="manifest"> tag.
Required Manifest Fields for Installability
| Field | Purpose | Example Value |
|---|---|---|
name |
Full app name shown in install prompt and app list | "Dev Station" |
short_name |
Name shown on home screen icon (under 12 chars recommended) | "DevStation" |
start_url |
Entry point when launched from home screen | "/?source=pwa" |
display |
Display mode: standalone, fullscreen, minimal-ui, or browser | "standalone" |
background_color |
Splash screen background color (hex) | "#ffffff" |
theme_color |
Browser UI and title bar color | "#2563EB" |
icons |
Array of icon objects with src, sizes, type, and purpose | 192px and 512px PNG plus maskable variant |
Beyond the required fields, several optional fields improve the installed experience. orientation locks the app to portrait or landscape. scope defines which URLs the PWA controls when launched. categories helps app stores classify your app. shortcuts defines quick-action menu items accessible from the app icon.
Maskable Icons
Include a maskable icon with "purpose": "maskable". Android uses adaptive icons that crop to different shapes, without a maskable variant, your icon may be cropped incorrectly on certain launchers.
Installability Criteria
Google requires: valid manifest with name, short_name, start_url, display standalone or fullscreen, 192px and 512px icons, a registered service worker with a fetch handler, and HTTPS. Meet all of these to trigger the install prompt.
Offline Support
Building Reliable Offline Experiences
Offline support is the defining feature of a PWA. But “offline” is not a binary state, users experience a spectrum of connectivity from full broadband to complete disconnection. A reliable PWA handles every point on that spectrum gracefully, providing cached content when the network fails and synchronizing data when connectivity returns.
The Offline Strategy Stack
Effective offline support combines several techniques working together. No single caching strategy handles every scenario; you layer them based on resource type and criticality:
| Layer | Technique | Handles |
|---|---|---|
| App Shell | Cache First, precache on install | Instant UI load on repeat visits |
| Static Assets | Cache First or Stale-While-Revalidate | Fonts, CSS, JS, images |
| API Data | Network First with cache fallback | Fresh data when online, cached when offline |
| User Content | IndexedDB with background sync | Form submissions, drafts, edits queued offline |
| Fallback Page | Cache Only, precached offline page | Branded message when no cached content matches |
Background Sync
Background Sync lets your PWA defer actions until the user has stable connectivity. When a user submits a form offline, the action is queued. Once the browser detects a connection, it fires a sync event in the service worker, giving your code a chance to retry the request. This is far better than showing an error and asking the user to try again later.
The registration is simple from the page: navigator.serviceWorker.ready.then(reg => reg.sync.register('submit-form')); . In the service worker, you listen for the sync event and attempt the deferred operation. If it fails, the browser will retry automatically with exponential backoff.
IndexedDB for Structured Offline Data
For anything beyond simple key-value caching, IndexedDB is the right storage mechanism. It stores structured data, supports transactions, and can hold hundreds of megabytes per origin. Libraries like IndexedDB or Dexie provide a promise-based API that is far easier to work with than the raw IndexedDB interface.
Key Insight, Cache storage (used by service workers) and IndexedDB are separate stores. Use Cache API for responses keyed by URL, perfect for the app shell and static assets. Use IndexedDB for structured application data like user drafts, form state, or synced records. Mixing them leads to stale data bugs.
Performance
Performance Optimization for PWAs
Performance is not optional for a PWA. It is an installability signal. Google’s Lighthouse tool scores PWA compliance, and a score below 90 means your app fails the quality bar. Performance optimization in a PWA context means optimizing both the initial load (before the service worker is installed) and repeat loads (where the service worker serves cached assets).
<100ms
Largest Contentful Paint (LCP) target
<200ms
First Input Delay (FID) target
<0.1
Cumulative Layout Shift (CLS) target
<170KB
Initial JavaScript budget (Alex Russell)
Optimization Checklist
| Technique | Impact | Implementation |
|---|---|---|
| Code splitting | Reduces initial JS payload | Dynamic import() per route |
| Image optimization | Largest payload savings | WebP or AVIF, responsive srcset, lazy loading |
| Resource hints | Faster critical path | preconnect and preload for critical origins |
| App shell precaching | Instant repeat loads | Cache HTML, CSS, core JS on SW install |
| Tree shaking | Eliminates dead code | Production build with ES modules |
| HTTP/2 or HTTP/3 | Parallel asset downloads | Server or CDN configuration |
| Gzip or Brotli | 30 to 70% text compression | Server-level compression |
Performance Tip, The service worker precache is a double-edged sword. Caching too many files on install slows down the first visit (the install happens on the main thread’s bandwidth). Cache only the app shell, the minimal HTML, CSS, and bootstrap JS needed to render the UI. Everything else should be cached lazily on first fetch using Stale-While-Revalidate.
Web Vitals Monitoring
Google’s Core Web Vitals (LCP, FID (now replaced by INP), and CLS) are the metrics that matter for search ranking and user experience. Instrument them in production using the web-vitals library, which provides accurate measurement aligned with how Chrome calculates them. Send the data to your analytics platform and set up alerts for regressions.
Deployment
Deploying and Updating Your PWA
Deploying a PWA involves the same steps as deploying any static web application, but with two critical additions: cache invalidation for the service worker itself, and a strategy for handling service worker updates without confusing users. Get these wrong and your users will be stuck on old versions indefinitely.
Deployment Checklist
1
Build the production bundle. Minify JavaScript, optimize images, generate the manifest with hashed icon filenames, and produce the service worker file with a precache manifest of versioned asset URLs.
2
Upload to a static host or CDN. Use a host that supports HTTPS by default, Netlify, Vercel, Cloudflare Pages, Firebase Hosting, or GitHub Pages all work. Ensure long-lived cache headers for hashed assets and short or no-cache headers for index.html and sw.js.
3
Verify the service worker serves correctly. The sw.js file must be served with a Cache-Control: no-cache or max-age=0 header. If the browser caches the SW file itself, users will never receive updates because the byte-for-byte comparison fails.
4
Test with Lighthouse. Run the PWA audit in Chrome DevTools or via the Lighthouse CLI. Confirm the installability criteria pass: manifest valid, service worker registered, HTTPS, and 90+ performance score.
5
Test on real devices. Install the PWA on an Android phone via Chrome, on iOS via Safari, and on desktop via Edge or Chrome. Verify offline behavior, push notifications, and that updates propagate within a reasonable window.
6
Set up monitoring. Track service worker update adoption, cache hit rates, and install conversion. Tools like Workbox’s Google Analytics integration or a custom beacon can surface when users are stuck on old versions.
Service Worker Update Flow
The update flow is a common source of bugs. When you deploy a new service worker, the browser detects the byte-level change, installs the new SW, but does not activate it until all tabs using the old SW are closed. This means users may not see your update for hours or days. Two approaches solve this:
| Approach | How It Works | When to Use |
|---|---|---|
skipWaiting() |
New SW activates immediately on install | Low-risk updates; combined with a page reload prompt |
| User prompt | Listen for controllerchange, show “Update available” toast |
Apps with critical state; let user decide when to reload |
| Versioned caches | Name caches with a version string; delete old on activate | Always, prevents cache bloat and stale content |
Key Insight, Never leave users on an old version silently. Either call skipWaiting() with a controlled reload, or implement an update-available notification. Silent staleness is the most common PWA support ticket. Users report bugs that were fixed in a deployment they never received.
Action
Next Steps to Ship Your PWA
You now have the full picture: manifest, service worker, offline strategies, performance budgets, and deployment. The path from a standard web app to a production PWA is well-defined and achievable in days, not weeks, if you follow the right sequence. Start with the installability criteria, add offline support incrementally, and measure relentlessly.
1
Audit your current app. Run Lighthouse PWA audit. Identify which installability criteria you are missing, manifest, service worker, HTTPS, or icons. This gives you a concrete gap list.
2
Add the web app manifest. Create manifest.json with required fields, generate 192px and 512px icons plus a maskable variant, and link it from your HTML head. Verify with Chrome DevTools Application tab.
3
Implement the service worker. Use Workbox or write a custom SW. Precache the app shell on install, implement Stale-While-Revalidate for assets, and Network First for API calls. Register it after the load event.
4
Add offline fallback. Create a branded offline page, precache it, and serve it as a navigation fallback when no cached route matches. This prevents the browser’s default offline dinosaur.
5
Optimize for Core Web Vitals. Measure LCP, INP, and CLS in production. Apply code splitting, image optimization, and resource hints. Set a performance budget and enforce it in CI.
6
Deploy and verify. Ship to your CDN with correct cache headers. Test installation on Android, iOS, and desktop. Monitor update adoption and cache hit rates for the first week.
Bottom Line, A PWA is not a rewrite. It is a set of progressive enhancements. You can add a manifest and basic service worker in a single afternoon and iterate from there. The installability criteria are well-documented, the tooling (Workbox, Lighthouse) is mature, and the user experience gains are measurable. Start small, ship, measure, and expand.
Dev Station works with teams across the United States and the United Kingdom. Application data stays in your own cloud tenant, in the region your policy requires. Where a client needs SOC 2, HIPAA or UK GDPR evidence, we build the technical controls those frameworks ask for and work alongside the assessor who issues the certificate. Our engineers work from Vietnam with overlap into US Eastern, US Pacific and UK GMT hours, and we invoice in USD or GBP.
Want an AI assistant to summarize or cite this guide?
Click any link below to open the AI with a pre-filled prompt referencing this article:
Talk To Us
Tell us what you are building and what it has to connect to. An engineer answers, and you get a straight view of what the work would take.
Get In Touch →


