Progressive Web Apps (PWAs)

Progressive Web Apps (PWAs) combine the best of web and mobile apps. They use modern web capabilities to deliver app-like experiences to users, with the reach and accessibility of the web.

PWA Architecture

Web App + Native-like Features = Progressive Web App

Core Principles of PWAs

Progressive Web Apps are built on three fundamental principles:

1. Reliable

Load instantly and never show the "dinosaur" offline page, even in uncertain network conditions.

2. Fast

Respond quickly to user interactions with silky smooth animations and scrolling.

3. Engaging

Feel like a natural app on the device, with an immersive user experience.

Key Technologies

1. Service Workers

Service Workers are JavaScript files that run separately from the main browser thread, intercepting network requests, caching resources, and enabling offline functionality.

// Register a service worker
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/service-worker.js')
      .then(registration => {
        console.log('Service Worker registered with scope:', registration.scope);
      })
      .catch(error => {
        console.error('Service Worker registration failed:', error);
      });
  });
}

Basic Service Worker Lifecycle

  1. Registration: The browser registers the service worker script.
  2. Installation: The service worker installs and typically caches static assets.
  3. Activation: The service worker activates and can control pages.
  4. Idle: The service worker goes idle when not in use.
  5. Termination: The browser may terminate the service worker to conserve memory.
// service-worker.js
const CACHE_NAME = 'my-pwa-cache-v1';
const urlsToCache = [
  '/',
  '/styles/main.css',
  '/scripts/main.js',
  '/images/logo.png'
];

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

// Fetch - Serve from cache, falling back to network
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        // Cache hit - return the response from the cached version
        if (response) {
          return response;
        }
        
        // Not in cache - fetch from network
        return fetch(event.request)
          .then(response => {
            // Check if we received a valid response
            if (!response || response.status !== 200 || response.type !== 'basic') {
              return response;
            }
            
            // Clone the response
            const responseToCache = response.clone();
            
            // Add to cache
            caches.open(CACHE_NAME)
              .then(cache => {
                cache.put(event.request, responseToCache);
              });
              
            return response;
          });
      })
  );
});

Note: Service Workers can only be used on websites served over HTTPS (except for localhost during development).

2. Web App Manifest

The Web App Manifest is a JSON file that provides information about a web application, allowing it to be installed on the home screen of a device.

// manifest.json
{
  "name": "My Progressive Web App",
  "short_name": "MyPWA",
  "description": "An example Progressive Web App",
  "start_url": "/index.html",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#4285f4",
  "icons": [
    {
      "src": "/images/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/images/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

Link the manifest in your HTML:

<link rel="manifest" href="/manifest.json">

Key Manifest Properties

  • name: The full name of the application.
  • short_name: A shorter name for the app (used on home screens).
  • start_url: The URL that loads when the app is launched.
  • display: How the app should be displayed (fullscreen, standalone, minimal-ui, browser).
  • background_color: The background color of the splash screen.
  • theme_color: The color of the browser UI elements.
  • icons: Array of icon objects with src, sizes, and type properties.

3. HTTPS

PWAs require secure connections. Service Workers only work on secure origins (HTTPS or localhost).

Tip: During development, you can use localhost. For production, always use HTTPS. Services like Let's Encrypt provide free SSL certificates.

Advanced PWA Features

Offline Capabilities

PWAs can work offline or with poor network connectivity using various caching strategies:

Strategy Description Best For
Cache First Check cache first, fall back to network Static assets that rarely change
Network First Try network first, fall back to cache Content that updates frequently
Stale While Revalidate Return from cache, then update cache from network Balance of performance and freshness
Cache Only Only serve from cache Assets that never change
Network Only Only serve from network Dynamic content that must be fresh

Push Notifications

PWAs can send push notifications to users, even when the browser is closed.

// Request permission for notifications
function requestNotificationPermission() {
  Notification.requestPermission().then(permission => {
    if (permission === 'granted') {
      console.log('Notification permission granted!');
      // Subscribe the user to push notifications
      subscribeToPushNotifications();
    }
  });
}

// Subscribe to push notifications
function subscribeToPushNotifications() {
  navigator.serviceWorker.ready
    .then(registration => {
      // Check for push subscription
      return registration.pushManager.getSubscription()
        .then(subscription => {
          if (subscription) {
            return subscription;
          }
          
          // Get the server's public key
          return fetch('/api/get-push-public-key')
            .then(response => response.json())
            .then(data => {
              const publicKey = data.publicKey;
              
              // Subscribe the user
              return registration.pushManager.subscribe({
                userVisibleOnly: true,
                applicationServerKey: urlBase64ToUint8Array(publicKey)
              });
            });
        })
        .then(subscription => {
          // Send the subscription to your server
          return fetch('/api/save-subscription', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json'
            },
            body: JSON.stringify(subscription)
          });
        })
        .then(response => {
          if (response.ok) {
            console.log('User subscribed to push notifications!');
          }
        });
    });
}

// Handle incoming push notifications in the service worker
// In service-worker.js
self.addEventListener('push', event => {
  const data = event.data.json();
  
  const options = {
    body: data.body,
    icon: '/images/notification-icon.png',
    badge: '/images/badge-icon.png',
    data: {
      url: data.url
    }
  };
  
  event.waitUntil(
    self.registration.showNotification(data.title, options)
  );
});

Background Sync

Background Sync allows web apps to defer actions until the user has stable connectivity.

// Register for background sync
function registerBackgroundSync() {
  navigator.serviceWorker.ready
    .then(registration => {
      return registration.sync.register('send-messages');
    })
    .then(() => {
      console.log('Background sync registered!');
    })
    .catch(err => {
      console.error('Background sync registration failed:', err);
    });
}

// In service-worker.js
self.addEventListener('sync', event => {
  if (event.tag === 'send-messages') {
    event.waitUntil(sendPendingMessages());
  }
});

// Function to send pending messages
function sendPendingMessages() {
  return fetch('/api/messages')
    .then(response => response.json())
    .then(data => {
      const promises = data.pendingMessages.map(message => {
        return fetch('/api/send-message', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(message)
        });
      });
      
      return Promise.all(promises);
    });
}

App Install Prompt

PWAs can prompt users to install the app on their device.

let deferredPrompt;

// Listen for the beforeinstallprompt event
window.addEventListener('beforeinstallprompt', event => {
  // Prevent the default prompt
  event.preventDefault();
  
  // Store the event for later use
  deferredPrompt = event;
  
  // Show your custom install button
  document.getElementById('install-button').style.display = 'block';
});

// Handle the install button click
document.getElementById('install-button').addEventListener('click', () => {
  // Hide the button
  document.getElementById('install-button').style.display = 'none';
  
  // Show the prompt
  deferredPrompt.prompt();
  
  // Wait for the user's choice
  deferredPrompt.userChoice.then(choiceResult => {
    if (choiceResult.outcome === 'accepted') {
      console.log('User accepted the install prompt');
    } else {
      console.log('User dismissed the install prompt');
    }
    
    // Clear the deferred prompt
    deferredPrompt = null;
  });
});

Testing and Auditing PWAs

Lighthouse

Lighthouse is an open-source tool from Google that audits web apps for performance, accessibility, and PWA features.

How to use Lighthouse:

  1. Open Chrome DevTools (F12 or Right-click > Inspect)
  2. Go to the "Lighthouse" tab
  3. Select "Progressive Web App" category
  4. Click "Generate report"

Workbox

Workbox is a set of libraries and tools that simplify the implementation of service workers and caching strategies.

// Using Workbox in service-worker.js
importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.1.5/workbox-sw.js');

// Cache the Google Fonts stylesheets
workbox.routing.registerRoute(
  ({url}) => url.origin === 'https://fonts.googleapis.com',
  new workbox.strategies.StaleWhileRevalidate({
    cacheName: 'google-fonts-stylesheets',
  })
);

// Cache the Google Fonts webfont files
workbox.routing.registerRoute(
  ({url}) => url.origin === 'https://fonts.gstatic.com',
  new workbox.strategies.CacheFirst({
    cacheName: 'google-fonts-webfonts',
    plugins: [
      new workbox.cacheableResponse.CacheableResponsePlugin({
        statuses: [0, 200],
      }),
      new workbox.expiration.ExpirationPlugin({
        maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year
        maxEntries: 30,
      }),
    ],
  })
);

// Cache CSS and JavaScript files
workbox.routing.registerRoute(
  ({request}) => request.destination === 'style' || request.destination === 'script',
  new workbox.strategies.StaleWhileRevalidate({
    cacheName: 'static-resources',
  })
);

// Cache images
workbox.routing.registerRoute(
  ({request}) => request.destination === 'image',
  new workbox.strategies.CacheFirst({
    cacheName: 'images',
    plugins: [
      new workbox.expiration.ExpirationPlugin({
        maxEntries: 60,
        maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
      }),
    ],
  })
);

PWA Best Practices

  • Responsive Design: Ensure your app works well on all device sizes.
  • App Shell Architecture: Implement an app shell that loads instantly, then loads content dynamically.
  • Offline First: Design with offline functionality in mind from the beginning.
  • Performance Optimization: Minimize file sizes, reduce network requests, and optimize loading.
  • Progressive Enhancement: Start with a basic experience that works everywhere, then enhance for modern browsers.
  • Cross-Browser Testing: Test your PWA across different browsers and devices.
  • Accessibility: Ensure your PWA is accessible to all users, including those with disabilities.

Warning: Don't cache everything! Be strategic about what you cache. Sensitive or frequently changing data should typically be fetched from the network.

Real-World PWA Examples

  • Twitter Lite: A PWA version of Twitter that's fast and data-efficient.
  • Starbucks: Their PWA allows ordering and payment, even offline.
  • Pinterest: Their PWA led to significant increases in engagement and ad revenue.
  • Spotify: Their PWA provides a lightweight alternative to the native app.
  • Uber: Their PWA works across various network conditions and devices.

Next Steps

Now that you understand Progressive Web Apps, you can explore:

  • Advanced caching strategies for complex applications
  • Implementing offline analytics
  • Creating app-like animations and transitions
  • Using IndexedDB for client-side data storage
  • Implementing PWA features in existing web applications