Practical Guide to Progressive Web Apps

A hands-on, comprehensive guide to building Progressive Web Apps—from service-worker fundamentals and caching strategies to offline-first architecture, push notifications, background sync, Workbox tooling and production-ready code examples.

1. Why Progressive Web Apps Matter

Users expect fast, reliable, app-like experiences regardless of network conditions or device capability. Progressive Web Apps deliver exactly that—using standard web technologies, a single codebase and no app-store gatekeeping.

Companies that have adopted PWAs report 50-80 % increases in engagement, 20-30 % higher conversion rates and dramatically lower development costs compared to maintaining separate native apps. PWAs are the pragmatic middle ground between a basic website and a full native application.

2. What Is a Progressive Web App?

A Progressive Web App is a web application that uses modern browser APIs—service workers, Web App Manifest, Cache Storage and push notifications—to deliver an experience that is fast, reliable, installable and engaging.

Key characteristics

  • Progressive: works for every user, regardless of browser, enhancing where supported.
  • Responsive: adapts to any form factor—phone, tablet, desktop.
  • Connectivity-independent: functions offline or on poor networks via service workers.
  • App-like: uses the app-shell model for native-feel navigation and interactions.
  • Fresh: always up to date thanks to the service-worker update flow.
  • Safe: served over HTTPS to guarantee integrity and prevent tampering.
  • Installable: users can add it to their home screen without an app store.
  • Re-engageable: push notifications bring users back to the app.
  • Linkable: shareable via URL, no installation required to access.

3. Core Components

ComponentPurposeRequired?
Service WorkerBackground script: caching, fetch interception, push, syncYes
Web App ManifestJSON metadata: name, icons, start URL, display mode, themeYes
HTTPSSecure context required by service workers and modern APIsYes
App ShellMinimal HTML/CSS/JS skeleton cached for instant first paintRecommended
Cache Storage APIProgrammatic cache for assets and API responsesYes (via SW)
IndexedDBClient-side structured storage for offline dataRecommended
Push API + Notifications APIServer-initiated messages and user-facing notificationsOptional
Background Sync APIDeferred network requests retried when connectivity returnsOptional

4. Service Worker Deep Dive

The service worker is the heart of every PWA. It is a JavaScript file that the browser runs in a separate thread, with no DOM access, acting as a programmable network proxy.

Lifecycle

  1. Registration: the page calls navigator.serviceWorker.register('/sw.js').
  2. Install: the browser downloads and parses the script. The install event fires—use it to pre-cache essential assets.
  3. Wait: if an older service worker is still controlling pages, the new one waits.
  4. Activate: the activate event fires. Clean up old caches here.
  5. Fetch: the service worker intercepts every network request from controlled pages.
  6. Update: the browser checks for a new SW file periodically. If bytes differ, the cycle restarts.

Scope

A service worker controls all pages under its scope (the directory where it is served). Place sw.js at the root to control the entire site.

5. Caching Strategies

Choosing the right caching strategy for each resource type is the most important PWA design decision. There is no one-size-fits-all answer.

StrategyHow It WorksBest For
Cache FirstServe from cache; fetch only on missStatic assets (CSS, JS, images, fonts)
Network FirstFetch from network; fall back to cache on failureAPI responses, dynamic HTML
Stale While RevalidateServe from cache immediately; update cache in backgroundSemi-dynamic content (avatars, feeds)
Network OnlyAlways fetch from network; no cachingAnalytics pings, non-GET requests
Cache OnlyServe from cache; never go to networkPre-cached app shell in offline mode

Cache versioning

// Name caches with a version. On activate, delete old ones.
const CACHE_VERSION = 'v3';
const CACHE_NAME    = `app-shell-${CACHE_VERSION}`;

self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(keys =>
      Promise.all(
        keys
          .filter(k => k.startsWith('app-shell-') && k !== CACHE_NAME)
          .map(k => caches.delete(k))
      )
    )
  );
  return self.clients.claim();  // take control immediately
});

6. Practical Code — Service Worker

A production-ready service worker implementing multiple caching strategies with proper versioning and fallback pages.

/* sw.js — Production service worker with multi-strategy caching */

const SHELL_CACHE  = 'shell-v2';
const DATA_CACHE   = 'data-v1';
const IMG_CACHE    = 'images-v1';

const SHELL_ASSETS = [
  '/',
  '/index.html',
  '/css/style.css',
  '/css/articles.css',
  '/js/main.js',
  '/offline.html',     // fallback page
];

// ── Install: pre-cache the app shell ────────────────────────────
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(SHELL_CACHE)
      .then(cache => cache.addAll(SHELL_ASSETS))
      .then(() => self.skipWaiting())
  );
});

// ── Activate: clean up old caches ───────────────────────────────
self.addEventListener('activate', event => {
  const keep = new Set([SHELL_CACHE, DATA_CACHE, IMG_CACHE]);
  event.waitUntil(
    caches.keys().then(keys =>
      Promise.all(keys.filter(k => !keep.has(k)).map(k => caches.delete(k)))
    ).then(() => self.clients.claim())
  );
});

// ── Fetch: route requests to the right strategy ─────────────────
self.addEventListener('fetch', event => {
  const { request } = event;
  const url = new URL(request.url);

  // API calls → Network First
  if (url.pathname.startsWith('/api/')) {
    event.respondWith(networkFirst(request, DATA_CACHE));
    return;
  }

  // Images → Cache First with size limit
  if (request.destination === 'image') {
    event.respondWith(cacheFirst(request, IMG_CACHE));
    return;
  }

  // Everything else (shell) → Cache First with offline fallback
  event.respondWith(
    cacheFirst(request, SHELL_CACHE).then(response =>
      response || caches.match('/offline.html')
    )
  );
});

// ── Strategy helpers ────────────────────────────────────────────
async function cacheFirst(request, cacheName) {
  const cached = await caches.match(request);
  if (cached) return cached;

  try {
    const response = await fetch(request);
    if (response.ok) {
      const cache = await caches.open(cacheName);
      cache.put(request, response.clone());
    }
    return response;
  } catch {
    return null;
  }
}

async function networkFirst(request, cacheName) {
  try {
    const response = await fetch(request);
    if (response.ok) {
      const cache = await caches.open(cacheName);
      cache.put(request, response.clone());
    }
    return response;
  } catch {
    return caches.match(request);
  }
}

7. Web App Manifest

The manifest tells the browser how to present your PWA when installed. It controls the app name, icons, splash screen, theme colour and display mode.

{
  "name": "My Progressive Web App",
  "short_name": "MyPWA",
  "description": "A fast, offline-capable task manager.",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#0a0e1a",
  "theme_color": "#00e5ff",
  "orientation": "any",
  "icons": [
    { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
    {
      "src": "/icons/icon-maskable-512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "maskable"
    }
  ],
  "screenshots": [
    { "src": "/screenshots/desktop.png", "sizes": "1280x720", "type": "image/png", "form_factor": "wide" },
    { "src": "/screenshots/mobile.png", "sizes": "750x1334", "type": "image/png", "form_factor": "narrow" }
  ]
}

Key fields

FieldPurposeNotes
displayHow the app appearsstandalone (most common), fullscreen, minimal-ui, browser
start_urlEntry point when launchedAdd a query param (?source=pwa) to track installs
theme_colorTitle bar / status bar colourMatch your brand colour
iconsHome screen and splash iconsInclude maskable icon for Android adaptive icons
screenshotsRicher install UIDesktop + mobile variants recommended

8. Installability & UX

Modern browsers show an install prompt when a PWA meets the installability criteria: valid manifest, registered service worker and HTTPS. You can enhance the experience by intercepting the prompt and showing it at the right moment.

// Deferred install prompt
let deferredPrompt = null;

window.addEventListener('beforeinstallprompt', e => {
  e.preventDefault();
  deferredPrompt = e;
  showInstallButton();          // show your custom UI
});

function handleInstallClick() {
  if (!deferredPrompt) return;
  deferredPrompt.prompt();
  deferredPrompt.userChoice.then(result => {
    console.log('Install outcome:', result.outcome);
    deferredPrompt = null;
    hideInstallButton();
  });
}

// Track successful installs
window.addEventListener('appinstalled', () => {
  console.log('PWA installed');
  // Send analytics event
});

Best practices

  • Show the install prompt after the user has engaged (not on first visit).
  • Explain the benefit: faster access, offline mode, notifications.
  • Provide a dismiss option and do not re-prompt for at least 30 days.
  • Track install conversion rate as a core metric.

9. Offline-First Architecture

Offline-first means designing for disconnection as the default state, not an exception. This inverts the traditional model: local data is the source of truth, and the network is used to sync when available.

Architecture layers

User interaction
  → Local state (IndexedDB / localStorage)
     → UI renders from local data (always fast)
     → Background sync queue (pending mutations)
        → Network request when online
           → Conflict resolution (last-write-wins / CRDT)
              → Server confirms & propagates

Storage options

APIData TypeMax SizeBest For
IndexedDBStructured (objects, blobs)Device-dependent (GBs)App data, offline queues
Cache StorageHTTP responsesDevice-dependentAssets, API response cache
localStorageStrings (key-value)~5-10 MBSimple settings, tokens
OPFS (Origin Private FS)Files (binary)Device-dependentLarge files, WASM data

10. Practical Code — Offline Data Sync

A complete offline-first data layer using IndexedDB for local storage and the Background Sync API for deferred network requests.

/* offline-sync.js — Offline-first data layer with background sync */

// ── IndexedDB helper (simplified) ───────────────────────────────
function openDB(name = 'pwa-store', version = 1) {
  return new Promise((resolve, reject) => {
    const req = indexedDB.open(name, version);
    req.onupgradeneeded = () => {
      const db = req.result;
      if (!db.objectStoreNames.contains('tasks')) {
        db.createObjectStore('tasks', { keyPath: 'id' });
      }
      if (!db.objectStoreNames.contains('sync-queue')) {
        db.createObjectStore('sync-queue', { autoIncrement: true });
      }
    };
    req.onsuccess = () => resolve(req.result);
    req.onerror   = () => reject(req.error);
  });
}

// ── Save locally + queue for sync ───────────────────────────────
async function saveTask(task) {
  const db = await openDB();
  const tx = db.transaction(['tasks', 'sync-queue'], 'readwrite');

  // 1. Write to local store (instant UI update)
  tx.objectStore('tasks').put(task);

  // 2. Queue for background sync
  tx.objectStore('sync-queue').add({
    url: '/api/tasks',
    method: 'POST',
    body: JSON.stringify(task),
    timestamp: Date.now(),
  });

  await tx.done;

  // 3. Request background sync (if supported)
  if ('serviceWorker' in navigator && 'SyncManager' in window) {
    const reg = await navigator.serviceWorker.ready;
    await reg.sync.register('sync-tasks');
  }
}

// ── Service worker: handle sync event ───────────────────────────
// (Add this to sw.js)
self.addEventListener('sync', event => {
  if (event.tag === 'sync-tasks') {
    event.waitUntil(flushSyncQueue());
  }
});

async function flushSyncQueue() {
  const db  = await openDB();
  const tx  = db.transaction('sync-queue', 'readwrite');
  const store = tx.objectStore('sync-queue');
  const items = await getAllFromStore(store);

  for (const item of items) {
    try {
      const res = await fetch(item.url, {
        method: item.method,
        headers: { 'Content-Type': 'application/json' },
        body: item.body,
      });
      if (res.ok) {
        store.delete(item.key);     // remove from queue on success
      }
    } catch {
      break;                        // stop on first failure (retry later)
    }
  }
}

function getAllFromStore(store) {
  return new Promise((resolve, reject) => {
    const req = store.getAll();
    req.onsuccess = () => resolve(req.result);
    req.onerror   = () => reject(req.error);
  });
}

11. Push Notifications

Push notifications re-engage users even when the PWA is not open. They use the Push API (server → service worker) and the Notifications API (service worker → user).

Flow

  1. Client subscribes via pushManager.subscribe() with a VAPID key.
  2. Server stores the subscription endpoint.
  3. Server sends a push message via the Web Push Protocol.
  4. Service worker receives the push event and shows a notification.
  5. User taps the notification → notificationclick event opens the app.
// In sw.js — handle push events
self.addEventListener('push', event => {
  const data = event.data?.json() ?? { title: 'New update', body: '' };
  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: '/icons/icon-192.png',
      badge: '/icons/badge-72.png',
      data: { url: data.url || '/' },
    })
  );
});

self.addEventListener('notificationclick', event => {
  event.notification.close();
  const url = event.notification.data.url;
  event.waitUntil(
    clients.matchAll({ type: 'window' }).then(list => {
      // Focus existing window or open new one
      for (const client of list) {
        if (client.url === url && 'focus' in client) return client.focus();
      }
      return clients.openWindow(url);
    })
  );
});

Best practices

  • Ask for permission only after the user shows intent (not on page load).
  • Make every notification actionable and valuable.
  • Provide a way to manage notification preferences inside the app.
  • Respect Notification.permission states: granted, denied, default.

12. Workbox — Production Tooling

Workbox is Google's library for generating and managing service workers. It abstracts caching strategies, precaching, routing and runtime caching into a concise, battle-tested API.

// workbox-config.js — Workbox build configuration
module.exports = {
  globDirectory: 'dist/',
  globPatterns: ['**/*.{html,css,js,png,svg,woff2}'],
  swDest: 'dist/sw.js',
  runtimeCaching: [
    {
      urlPattern: /\/api\//,
      handler: 'NetworkFirst',
      options: {
        cacheName: 'api-cache',
        expiration: { maxEntries: 50, maxAgeSeconds: 300 },
        networkTimeoutSeconds: 3,
      },
    },
    {
      urlPattern: /\.(?:png|jpg|webp|svg)$/,
      handler: 'CacheFirst',
      options: {
        cacheName: 'image-cache',
        expiration: { maxEntries: 100, maxAgeSeconds: 30 * 24 * 3600 },
      },
    },
  ],
};

// Generate: npx workbox generateSW workbox-config.js

Why Workbox?

  • Eliminates common service-worker bugs (stale caches, missing headers).
  • Integrates with Webpack, Vite, Rollup and CLI.
  • Provides workbox-precaching with revision hashing out of the box.
  • Supports workbox-background-sync for reliable offline mutations.

13. Performance Optimisation

Core Web Vitals targets

MetricGoodPWA Impact
LCP (Largest Contentful Paint)≤ 2.5 sApp shell from cache → near-instant LCP on repeat visits
INP (Interaction to Next Paint)≤ 200 msMinimal JS on main thread, defer non-critical code
CLS (Cumulative Layout Shift)≤ 0.1Cached fonts and sized images prevent layout shifts

Optimisation checklist

  • Pre-cache the app shell (HTML + critical CSS + core JS) during service-worker install.
  • Use defer or async for all non-critical scripts.
  • Serve images in WebP/AVIF with explicit width and height attributes.
  • Enable Brotli / gzip compression on the server.
  • Inline critical CSS and lazy-load below-the-fold styles.
  • Use font-display: swap to prevent invisible text during font load.
  • Prefetch navigation targets the user is likely to visit next.

14. Testing & Auditing

Lighthouse

Run Lighthouse in Chrome DevTools or via CLI. The PWA audit checks service-worker registration, manifest validity, HTTPS, offline fallback and more.

# Run Lighthouse from CLI
npx lighthouse https://example.com --output html --output-path report.html

# PWA-specific audit
npx lighthouse https://example.com --only-categories=pwa

Testing matrix

What to TestTool / Method
Offline behaviourChrome DevTools → Network → Offline toggle
Cache contentsApplication → Cache Storage panel
SW lifecycleApplication → Service Workers (update on reload, skip waiting)
Install promptManifest audit + Lighthouse PWA section
Push notificationsApplication → Service Workers → Push test button
PerformanceLighthouse, WebPageTest, CrUX dashboard

15. PWA vs Native vs Hybrid

DimensionPWANative (iOS/Android)Hybrid (Capacitor/RN)
DistributionURL (no store needed)App storesApp stores + URL
InstallationOptional, instantRequired downloadRequired download
OfflineService workerFull native storageBoth
PerformanceGood (improving)BestNear-native
Device APIsGrowing (Bluetooth, USB, NFC, sensors)Full accessFull access
Development costLow (single codebase)High (per platform)Medium
UpdatesInstant (no review)Store review cycleStore review cycle
DiscoverabilitySEO, URL sharingStore searchBoth

16. Real-World Use Cases

E-commerce

PWA storefronts load instantly on slow mobile networks, support offline product browsing and achieve higher conversion via add-to- home-screen. Examples: Starbucks, Alibaba, Flipkart.

News & media

News PWAs cache the latest articles and images, letting readers browse content on the tube or a plane. Push notifications drive re-engagement for breaking news.

Productivity tools

Task managers, note-taking apps and project trackers use offline-first sync to ensure edits are never lost, even without connectivity.

Social platforms

Twitter Lite (now X Lite) pioneered the PWA approach: 65 % increase in pages per session, 75 % increase in tweets sent and a 20 % decrease in bounce rate.

Internal enterprise apps

Field workers in logistics, healthcare and utilities use PWAs that work offline in warehouses, hospitals and rural areas, syncing when connectivity returns.

17. Future Directions

  • Project Fugu APIs: ongoing Chromium effort to close the gap with native—File System Access, Bluetooth, USB, NFC, screen wake lock and more.
  • iOS improvements: Apple is incrementally expanding PWA support—push notifications (iOS 16.4+), badge API, improved SW behaviour.
  • Install Richer UI: screenshots and descriptions in the install dialog for a store-like experience.
  • WASM integration: running compute-heavy tasks (image processing, ML inference) in the service worker via WebAssembly.
  • Declarative service workers: proposals for JSON-based SW configuration, reducing the need for imperative code.
  • Mini-apps & super-apps: platform vendors exploring PWA-based mini-app ecosystems.

18. Frequently Asked Questions

Do PWAs work on iOS?
Yes. Safari supports service workers, Cache Storage, IndexedDB and Web App Manifest. Push notifications are supported from iOS 16.4. Some APIs (e.g. Background Sync) are still missing.
Can a PWA be listed in the App Store or Google Play?
Yes. Tools like PWABuilder and Bubblewrap wrap your PWA in a Trusted Web Activity (Android) or a native shell (iOS), letting you publish to stores while keeping the web codebase.
How much offline storage is available?
It varies by browser and device. Chrome allows up to 80 % of available disk. Use navigator.storage.estimate() to check quota and usage at runtime.
Will a service worker slow down my site?
No. A well-written service worker makes repeat visits faster by serving assets from cache. The SW runs in a separate thread and does not block the main thread.
How do I update the PWA for users?
The browser automatically checks for a new sw.js on navigation. When bytes differ, the new SW installs in the background. Use skipWaiting() + clients.claim() for immediate takeover, or prompt the user to refresh.
What about SEO?
PWAs are standard web pages—fully indexable by search engines. In fact, their performance improvements (fast loads, reduced bounce) often boost SEO rankings.
Do I need Workbox?
Not strictly, but it is highly recommended for production. It handles cache versioning, routing, precaching with revision hashing and background sync—eliminating an entire class of common bugs.

19. Glossary

Service Worker
A browser-managed JavaScript worker that intercepts network requests and enables offline caching, push notifications and background sync.
Web App Manifest
A JSON file that describes a PWA's metadata (name, icons, display mode, theme) so browsers can offer installation.
App Shell
The minimal HTML, CSS and JS needed to render the UI skeleton, cached for instant first paint on repeat visits.
Cache Storage API
A browser API for programmatically storing and retrieving HTTP request/response pairs.
IndexedDB
A low-level browser API for storing large amounts of structured data, including files and blobs.
Background Sync
An API that defers network requests until connectivity is restored, ensuring data is never lost.
VAPID
Voluntary Application Server Identification—a key pair used to authenticate push notification senders.
Workbox
Google's library for generating and managing production service workers with built-in caching strategies.
Stale While Revalidate
A caching strategy that serves cached content immediately and updates the cache in the background.
Trusted Web Activity (TWA)
An Android integration that lets a PWA run in a full-screen Chrome-based activity, publishable to Google Play.
Core Web Vitals
Google's key metrics for page experience: LCP, INP and CLS.

20. References & Further Reading

Progressive Web Apps combine the reach of the web with the reliability of native apps. Start with a manifest and a service worker that caches your app shell, measure with Lighthouse, then layer in offline data sync and push notifications. Share this guide with your team and start building your PWA today.