Back to articles
May 21, 2026

PWA — Progressively Enhanced Apps

PWA — Progressively Enhanced Apps Progressive Web Apps (PWAs) combine the best of web and native apps. They work offline, load quickly, can be installed on the home screen, and send push…

Placeholder cover imagePhoto: Lorem Picsum / Unsplash

PWA — Progressively Enhanced Apps

Progressive Web Apps (PWAs) combine the best of web and native apps. They work offline, load quickly, can be installed on the home screen, and send push notifications — all without an app store. This guide walks through building a PWA from scratch.

What Makes a PWA

A PWA must meet three core criteria:

  1. Secure: Served over HTTPS
  2. Installable: Has a web app manifest and meets browser requirements
  3. Offline-capable: Uses a service worker to cache resources

These features are progressive, meaning they enhance the experience for supporting browsers while degrading gracefully for older ones.

The Web App Manifest

The manifest is a JSON file that tells the browser how your app should behave when installed:

{
  "name": "My PWA App",
  "short_name": "MyPWA",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#2563eb",
  "orientation": "portrait-primary",
  "icons": [
    {
      "src": "/icons/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

Link it in your HTML:

<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#2563eb">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">

Service Worker Basics

The service worker is a JavaScript file that runs in the background, separate from the web page. It handles caching, push notifications, and background sync.

// sw.js — Service Worker

const CACHE_NAME = 'pwa-v1';
const ASSETS_TO_CACHE = [
  '/',
  '/index.html',
  '/styles/main.css',
  '/scripts/app.js',
  '/icons/icon-192.png',
  '/icons/icon-512.png'
];

// Install event — cache assets
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS_TO_CACHE))
  );
  self.skipWaiting();
});

// Activate event — clean old caches
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(
        keys
          .filter((key) => key !== CACHE_NAME)
          .map((key) => caches.delete(key))
      )
    )
  );
  self.clients.claim();
});

// Fetch event — serve from cache, fall back to network
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => {
      return cached || fetch(event.request).then((response) => {
        // Clone the response for caching
        const clone = response.clone();
        caches.open(CACHE_NAME).then((cache) => {
          cache.put(event.request, clone);
        });
        return response;
      });
    })
  );
});

Register the service worker in your main JavaScript:

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js')
      .then((registration) => {
        console.log('SW registered:', registration.scope);
      })
      .catch((error) => {
        console.log('SW registration failed:', error);
      });
  });
}

Offline Experience

A great PWA doesn't just work offline — it tells the user when it does:

// Show offline banner
window.addEventListener('offline', () => {
  document.getElementById('offline-banner').style.display = 'block';
});

window.addEventListener('online', () => {
  document.getElementById('offline-banner').style.display = 'none';
  window.location.reload();
});
<div id="offline-banner" style="display:none; background:#f59e0b; color:#000; text-align:center; padding:0.5rem;">
  You are offline. Some features may be limited.
</div>

Push Notifications

Enable real-time engagement with push notifications:

// Request permission
async function requestNotificationPermission() {
  if ('Notification' in window && Notification.permission === 'default') {
    const permission = await Notification.requestPermission();
    if (permission === 'granted') {
      const subscription = await navigator.serviceWorker.ready
        .then(reg => reg.pushManager.subscribe({
          userVisibleOnly: true,
          applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
        }));
      console.log('Subscription:', subscription);
    }
  }
}

// Show notification
self.addEventListener('push', (event) => {
  const data = event.data?.json() || { title: 'Update', body: 'New content available' };
  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: '/icons/icon-192.png',
      badge: '/icons/icon-192.png'
    })
  );
});

Conclusion

PWAs bridge the gap between web and native apps, offering installability, offline support, and push notifications without the friction of app stores. Start with the manifest and a basic service worker, then gradually add features like background sync and push notifications. Test with Lighthouse's PWA audit to ensure you're hitting all the criteria. The return on investment is significant — users love the app-like experience, and you maintain the reach and discoverability of the open web.