Technical Foundations of PWAs

Service Workers

The backbone of Progressive Web Apps:

What Are Service Workers?:

  • JavaScript files that run separately from the main browser thread
  • Act as network proxies between web applications and the network
  • Enable offline functionality and background processing
  • Persist beyond page refreshes and browser restarts
  • Event-driven architecture with lifecycle events

Service Worker Lifecycle:

  • Registration
  • Installation
  • Activation
  • Idle
  • Termination
  • Update

Example Service Worker Registration:

// Check if service workers are supported
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);
      });
  });
}

Example Service Worker Implementation:

// service-worker.js
const CACHE_NAME = 'my-pwa-cache-v1';
const urlsToCache = [
  '/',
  '/index.html',
  '/styles/main.css',
  '/scripts/main.js',
  '/images/logo.png',
  '/offline.html'
];

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

// Activate event - clean up old caches
self.addEventListener('activate', event => {
  const cacheWhitelist = [CACHE_NAME];
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            return caches.delete(cacheName);
          }
        })
      );
    }).then(() => self.clients.claim())
  );
});

// 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;
        }
        
        // Clone the request
        const fetchRequest = event.request.clone();
        
        return fetch(fetchRequest)
          .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 failed, serve offline page
            if (event.request.mode === 'navigate') {
              return caches.match('/offline.html');
            }
          });
      })
  );
});

Service Worker Capabilities:

  • Caching strategies
  • Background sync
  • Push notifications
  • Content prefetching
  • Navigation preload
  • Periodic background sync

Web App Manifest

Defining the installable experience:

Purpose of the Manifest:

  • Enables “Add to Home Screen” functionality
  • Defines how the app appears when installed
  • Specifies launch behavior and orientation
  • Sets theme colors and icons
  • Controls display mode (fullscreen, standalone, etc.)

Example Web App Manifest:

{
  "name": "Weather PWA",
  "short_name": "Weather",
  "description": "Weather forecast information",
  "start_url": "/index.html",
  "display": "standalone",
  "background_color": "#3E4EB8",
  "theme_color": "#2F3BA2",
  "orientation": "portrait-primary",
  "icons": [
    {
      "src": "/images/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/images/icons/icon-96x96.png",
      "sizes": "96x96",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/images/icons/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/images/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/images/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ],
  "shortcuts": [
    {
      "name": "Today's Weather",
      "short_name": "Today",
      "description": "View today's weather forecast",
      "url": "/today",
      "icons": [{ "src": "/images/today.png", "sizes": "192x192" }]
    },
    {
      "name": "Weekly Forecast",
      "short_name": "Week",
      "description": "View weekly weather forecast",
      "url": "/week",
      "icons": [{ "src": "/images/week.png", "sizes": "192x192" }]
    }
  ]
}

Key Manifest Properties:

  • name: Full application name
  • short_name: Name for home screen
  • icons: App icons in various sizes
  • start_url: Initial URL when launched
  • display: Presentation mode
  • background_color: Splash screen color
  • theme_color: UI theme color
  • orientation: Preferred orientation

Linking the Manifest:

<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#2F3BA2">
<link rel="apple-touch-icon" href="/images/icons/icon-192x192.png">

Caching Strategies

Approaches for reliable content delivery:

Common Caching Strategies:

  • Cache First: Check cache before network
  • Network First: Try network, fall back to cache
  • Stale-While-Revalidate: Serve cached, update in background
  • Cache Only: Only serve from cache
  • Network Only: Only serve from network

Example Cache-First Strategy:

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        // Return cached response if found
        if (response) {
          return response;
        }
        
        // Otherwise fetch from network
        return fetch(event.request)
          .then(response => {
            // Don't cache if not valid response
            if (!response || response.status !== 200 || response.type !== 'basic') {
              return response;
            }
            
            // Clone and cache the response
            const responseToCache = response.clone();
            caches.open(CACHE_NAME)
              .then(cache => {
                cache.put(event.request, responseToCache);
              });
              
            return response;
          });
      })
  );
});

Example Stale-While-Revalidate Strategy:

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.open(CACHE_NAME).then(cache => {
      return cache.match(event.request).then(cachedResponse => {
        const fetchPromise = fetch(event.request).then(networkResponse => {
          // Update cache with fresh response
          cache.put(event.request, networkResponse.clone());
          return networkResponse;
        });
        
        // Return cached response immediately, or wait for network
        return cachedResponse || fetchPromise;
      });
    })
  );
});

Choosing the Right Strategy:

  • Use Cache First for static assets
  • Use Network First for API requests
  • Use Stale-While-Revalidate for content that updates periodically
  • Use Cache Only for offline-specific assets
  • Use Network Only for non-cacheable content