PWA - Progressive Web Apps — Build App-Like Experiences on the Web

PWA - Progressive Web Apps — Build App-Like Experiences on the Web

11/10/2025 Web By Tech Writers
Progressive Web AppsService WorkersWeb DevelopmentOffline SupportMobile WebJavaScriptPerformance

Introduction: The Future of Web Applications

Progressive Web Apps (PWAs) bridge the gap between web applications and native apps, delivering the best of both worlds. PWAs work offline, can be installed on home screens, send push notifications, and run at native-like speeds. This comprehensive guide covers everything you need to build modern PWAs.

Table of Contents

What is a PWA?

A Progressive Web App combines web and app features to deliver an enhanced user experience. PWAs are:

  • Progressive: Work on all devices and browsers
  • Responsive: Perfect fit for screen size
  • Connectivity Independent: Work offline via service workers
  • App-like: Navigation and interactions feel native
  • Fresh: Always up-to-date thanks to service workers
  • Safe: Served via HTTPS
  • Discoverable: Identifiable as “application”
  • Installable: Home screen installation without app store
  • Linkable: Easy sharing via URL

Why PWAs Matter

PWAs solve critical challenges:

  • Offline Support: No internet? No problem
  • Reduced Data Usage: Cached content means less bandwidth
  • Faster Loading: Cached resources load instantly
  • Native-like Feel: Full-screen, home screen icon
  • Push Notifications: Re-engagement capabilities
  • No App Store: Direct distribution via web

PWA Core Requirements

1. HTTPS Security

All PWAs must be served over HTTPS. This ensures secure communication and enables service workers.

PWAs require HTTPS
Exceptions: localhost for development
Certificate: Can use Let's Encrypt (free)
Validation: Check browser console for HTTPS warnings

2. Service Worker Registration

Service workers act as a proxy between your app and the network, enabling offline functionality and caching.

// Register service worker
if ('serviceWorker' in navigator) {
  navigator.serviceWorker
    .register('/sw.js')
    .then(registration => {
      console.log('Service Worker registered:', registration);
    })
    .catch(error => {
      console.error('Service Worker registration failed:', error);
    });
}

3. Web App Manifest

The manifest file tells the browser how to display your app when installed.

{
  "name": "My Progressive Web App",
  "short_name": "MyApp",
  "description": "An amazing PWA example",
  "start_url": "/",
  "scope": "/",
  "display": "standalone",
  "orientation": "portrait",
  "background_color": "#ffffff",
  "theme_color": "#000000",
  "icons": [
    {
      "src": "/images/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/images/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/images/icon-maskable.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "maskable"
    }
  ]
}

Adding Manifest to HTML

Link the manifest file in your HTML head section.

<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#000000">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="apple-touch-icon" href="/images/apple-touch-icon.png">

Service Workers

Service workers are the backbone of PWAs, running in the background and handling caching, offline support, and background sync.

Service Worker Lifecycle

Installation  →  Activation  →  Fetch/Message Handling  →  Termination
(cache setup)   (cleanup old)   (serve from cache)     (if unused)

Implementing Service Worker Caching

// sw.js - Service Worker
const CACHE_NAME = 'app-cache-v1';
const urlsToCache = [
  '/',
  '/index.html',
  '/css/styles.css',
  '/js/app.js',
  '/images/logo.png'
];

// Install event - cache assets
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => {
      console.log('Opened cache');
      return cache.addAll(urlsToCache);
    })
  );
});

// Activate event - cleanup old caches
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (cacheName !== CACHE_NAME) {
            console.log('Deleting old cache:', cacheName);
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

// Fetch event - serve from cache, fall back to network
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(response => {
      // Cache hit - return response
      if (response) {
        return response;
      }
      
      return fetch(event.request).then(response => {
        // Check if valid response
        if (!response || response.status !== 200 || response.type !== 'basic') {
          return response;
        }
        
        // Clone the response
        const responseToCache = response.clone();
        
        caches.open(CACHE_NAME).then(cache => {
          cache.put(event.request, responseToCache);
        });
        
        return response;
      });
    }).catch(() => {
      // Network request failed, return offline page
      return caches.match('/offline.html');
    })
  );
});

Strategies for Caching

Cache First (Cache, falling back to network)

  • Best for: Assets that don’t change often
  • Speed: Fast offline experience
  • Use case: Images, CSS, JavaScript

Network First (Network, falling back to cache)

  • Best for: Frequently updated content
  • Speed: Always gets fresh data when available
  • Use case: API responses, dynamic content

Stale While Revalidate

  • Best for: Content that can be slightly out of date
  • Speed: Fast initial load, background update
  • Use case: Blog posts, news feeds

Web Manifest

The web manifest defines how your app appears when installed and how it should behave.

Display Modes

"display": "standalone"      // Like a native app
"display": "fullscreen"      // Full screen, no browser chrome
"display": "minimal-ui"      // Minimal UI
"display": "browser"         // Normal browser

Theme Customization

{
  "theme_color": "#000000",       // Browser UI color
  "background_color": "#ffffff",  // Background during load
  "scope": "/app/",              // PWA scope
  "start_url": "/app/"           // Launch URL
}

Offline Functionality

Offline Detection

// Detect online/offline status
window.addEventListener('online', () => {
  console.log('User is online');
  syncPendingData();
});

window.addEventListener('offline', () => {
  console.log('User is offline');
  showOfflineMessage();
});

// Check current status
if (navigator.onLine) {
  console.log('Currently online');
} else {
  console.log('Currently offline');
}

Local Storage and IndexedDB

Store data locally for offline access.

// IndexedDB for complex data
const dbRequest = indexedDB.open('myapp', 1);

dbRequest.onsuccess = event => {
  const db = event.target.result;
  const transaction = db.transaction(['items'], 'readwrite');
  const store = transaction.objectStore('items');
  
  store.put({ id: 1, title: 'Item 1' });
};

Push Notifications

Requesting Permission

// Request notification permission
Notification.requestPermission().then(permission => {
  if (permission === 'granted') {
    console.log('Notification permission granted');
  }
});

Sending Notifications

// Service Worker push handler
self.addEventListener('push', event => {
  const options = {
    body: event.data.text(),
    icon: '/images/icon-192x192.png',
    badge: '/images/badge-72x72.png',
    tag: 'notification',
    requireInteraction: false
  };
  
  event.waitUntil(
    self.registration.showNotification('New Message', options)
  );
});

// Handle notification clicks
self.addEventListener('notificationclick', event => {
  event.notification.close();
  
  event.waitUntil(
    clients.matchAll({ type: 'window' }).then(clientList => {
      for (let client of clientList) {
        if (client.url === '/' && 'focus' in client) {
          return client.focus();
        }
      }
      if (clients.openWindow) {
        return clients.openWindow('/');
      }
    })
  );
});

Installation and Distribution

Add to Home Screen

Users can install your PWA directly:

  1. Visit your PWA in browser
  2. Tap the install prompt (or menu → “Add to Home Screen”)
  3. App appears like native app

Web App Install Banners

Show custom install prompts.

let deferredPrompt;

window.addEventListener('beforeinstallprompt', event => {
  event.preventDefault();
  deferredPrompt = event;
  
  // Show custom install button
  document.getElementById('install-btn').style.display = 'block';
  
  document.getElementById('install-btn').addEventListener('click', () => {
    deferredPrompt.prompt();
    deferredPrompt.userChoice.then(choice => {
      if (choice.outcome === 'accepted') {
        console.log('User installed PWA');
      }
      deferredPrompt = null;
    });
  });
});

window.addEventListener('appinstalled', () => {
  console.log('PWA was installed');
});

Performance Optimization

Key Metrics for PWAs

  • First Contentful Paint (FCP): < 1.8s
  • Largest Contentful Paint (LCP): < 2.5s
  • Cumulative Layout Shift (CLS): < 0.1
  • Time to Interactive (TTI): < 3.8s

Performance Checklist

✓ Service worker precaches critical assets ✓ Images optimized and lazy-loaded ✓ Code splitting and bundling optimized ✓ Network requests minimized ✓ Offline experience tested ✓ Performance monitored in production

  • Twitter Lite: 30% data reduction, fast loading
  • Spotify: Full offline support, home screen
  • Pinterest: 40% faster on 3G, home screen install
  • Starbucks: Order ahead offline, sync on reconnect
  • Washington Post: 1MB total size, fast load times

Debugging PWAs

Chrome DevTools

  1. Application Tab: View manifest, service workers, storage
  2. Service Workers: Check status, unregister, skip waiting
  3. Storage: Inspect cache, local storage, IndexedDB
  4. Offline Simulation: Test offline behavior

Testing Offline

// Network throttling test
// DevTools → Network → Throttling
// Check your app still works

Best Practices

✓ Always serve over HTTPS ✓ Provide offline fallback pages ✓ Optimize manifest icons for all sizes ✓ Test on real devices ✓ Monitor performance metrics ✓ Keep service workers updated ✓ Handle service worker errors gracefully ✓ Provide clear uninstall options

PWAs represent the future of web delivery!


Last updated: January 8, 2026

What is a PWA?

A Progressive Web App is a web application that:

  • Works offline seamlessly
  • Is installable on home screen
  • Provides app-like experience
  • Is fast and reliable

Requirements

1. HTTPS

All PWAs must be served over HTTPS

2. Service Worker

// Register service worker
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js');
}

3. Web Manifest

{
  "name": "My App",
  "short_name": "App",
  "icons": [
    {
      "src": "/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    }
  ],
  "start_url": "/",
  "display": "standalone"
}

Service Worker Caching

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open('v1').then(cache => {
      return cache.addAll([
        '/',
        '/styles.css',
        '/app.js'
      ]);
    })
  );
});

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => response || fetch(event.request))
  );
});

Features

✓ Offline support without network ✓ Home screen installation ✓ Push notifications ✓ Background sync ✓ App shell architecture

  • Twitter Lite
  • Spotify
  • Pinterest
  • Starbucks

PWAs are the future of web development!