Progressive Web App Implementation

1. Service Worker Workbox Caching

Implement service workers with Google Workbox for offline functionality, caching strategies, and background resource management.

Strategy Use Case Behavior Fallback
Network First API calls, dynamic content Try network, fallback to cache Stale data on offline
Cache First Static assets (CSS, JS, images) Serve from cache, update in background Fast but potentially stale
Stale While Revalidate Frequently updated content Serve cache, fetch fresh copy Balance speed & freshness
Network Only Real-time data, auth requests Always fetch from network Fail if offline
Cache Only Pre-cached app shell Only serve from cache Fast, no network needed

Example: Workbox service worker setup

// Installation
npm install workbox-webpack-plugin --save-dev

// webpack.config.js
const WorkboxPlugin = require('workbox-webpack-plugin');

module.exports = {
  plugins: [
    new WorkboxPlugin.GenerateSW({
      clientsClaim: true,
      skipWaiting: true,
      runtimeCaching: [
        {
          urlPattern: /^https:\/\/api\.example\.com/,
          handler: 'NetworkFirst',
          options: {
            cacheName: 'api-cache',
            expiration: {
              maxEntries: 50,
              maxAgeSeconds: 5 * 60 // 5 minutes
            },
            networkTimeoutSeconds: 10
          }
        },
        {
          urlPattern: /\.(?:png|jpg|jpeg|svg|gif)$/,
          handler: 'CacheFirst',
          options: {
            cacheName: 'image-cache',
            expiration: {
              maxEntries: 60,
              maxAgeSeconds: 30 * 24 * 60 * 60 // 30 days
            }
          }
        },
        {
          urlPattern: /\.(?:js|css)$/,
          handler: 'StaleWhileRevalidate',
          options: {
            cacheName: 'static-resources'
          }
        }
      ]
    })
  ]
};

// Register service worker in app
// index.tsx
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('/service-worker.js')
      .then(registration => {
        console.log('SW registered:', registration);
        
        // Check for updates
        registration.addEventListener('updatefound', () => {
          const newWorker = registration.installing;
          newWorker?.addEventListener('statechange', () => {
            if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
              // New version available
              showUpdateNotification();
            }
          });
        });
      })
      .catch(error => {
        console.error('SW registration failed:', error);
      });
  });
}

Example: Custom Workbox service worker

// service-worker.js
import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { NetworkFirst, CacheFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';

// Clean up old caches
cleanupOutdatedCaches();

// Precache assets from build
precacheAndRoute(self.__WB_MANIFEST);

// Cache API responses with NetworkFirst
registerRoute(
  ({ url }) => url.pathname.startsWith('/api/'),
  new NetworkFirst({
    cacheName: 'api-cache',
    plugins: [
      new CacheableResponsePlugin({
        statuses: [0, 200]
      }),
      new ExpirationPlugin({
        maxEntries: 50,
        maxAgeSeconds: 5 * 60,
        purgeOnQuotaError: true
      })
    ]
  })
);

// Cache images with CacheFirst
registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'image-cache',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 100,
        maxAgeSeconds: 30 * 24 * 60 * 60
      })
    ]
  })
);

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

// Offline fallback page
const FALLBACK_HTML_URL = '/offline.html';
const CACHE_NAME = 'offline-fallbacks';

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => cache.add(FALLBACK_HTML_URL))
  );
});

self.addEventListener('fetch', (event) => {
  if (event.request.mode === 'navigate') {
    event.respondWith(
      fetch(event.request).catch(() => {
        return caches.match(FALLBACK_HTML_URL);
      })
    );
  }
});

Example: React hook for service worker updates

// useServiceWorker.ts
import { useEffect, useState } from 'react';

export function useServiceWorker() {
  const [updateAvailable, setUpdateAvailable] = useState(false);
  const [registration, setRegistration] = useState<ServiceWorkerRegistration | null>(null);

  useEffect(() => {
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/service-worker.js').then((reg) => {
        setRegistration(reg);

        // Check for updates every hour
        setInterval(() => {
          reg.update();
        }, 60 * 60 * 1000);

        reg.addEventListener('updatefound', () => {
          const newWorker = reg.installing;
          newWorker?.addEventListener('statechange', () => {
            if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
              setUpdateAvailable(true);
            }
          });
        });
      });
    }
  }, []);

  const updateServiceWorker = () => {
    if (registration?.waiting) {
      registration.waiting.postMessage({ type: 'SKIP_WAITING' });
      window.location.reload();
    }
  };

  return { updateAvailable, updateServiceWorker };
}

// Usage in component
function App() {
  const { updateAvailable, updateServiceWorker } = useServiceWorker();

  return (
    <div>
      {updateAvailable && (
        <div className="update-banner">
          <p>A new version is available!</p>
          <button onClick={updateServiceWorker}>Update Now</button>
        </div>
      )}
      {/* App content */}
    </div>
  );
}
Best Practices: Use NetworkFirst for API calls, CacheFirst for static assets, StaleWhileRevalidate for frequently updated content. Always provide offline fallback pages. Test with DevTools offline mode.

2. Web App Manifest Installation

Configure Web App Manifest for installable PWA with app-like experience, custom icons, splash screens, and display modes.

Property Purpose Required Example
name Full app name Yes "My Progressive Web App"
short_name Home screen name Yes "My PWA"
icons App icons (various sizes) Yes 192x192, 512x512 PNG
start_url Launch URL Yes "/?source=pwa"
display Display mode Yes standalone, fullscreen
theme_color Browser UI color No "#007acc"
background_color Splash screen color No "#ffffff"
orientation Screen orientation No portrait, landscape

Example: Complete manifest.json configuration

// public/manifest.json
{
  "name": "My Progressive Web Application",
  "short_name": "My PWA",
  "description": "A modern progressive web app with offline support",
  "start_url": "/?source=pwa",
  "display": "standalone",
  "theme_color": "#007acc",
  "background_color": "#ffffff",
  "orientation": "portrait-primary",
  "icons": [
    {
      "src": "/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/icons/icon-96x96.png",
      "sizes": "96x96",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-144x144.png",
      "sizes": "144x144",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-152x152.png",
      "sizes": "152x152",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/icons/icon-384x384.png",
      "sizes": "384x384",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any 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"
    }
  ],
  "categories": ["productivity", "utilities"],
  "shortcuts": [
    {
      "name": "New Document",
      "short_name": "New",
      "description": "Create a new document",
      "url": "/new?source=pwa",
      "icons": [{ "src": "/icons/new.png", "sizes": "96x96" }]
    },
    {
      "name": "Dashboard",
      "short_name": "Dashboard",
      "description": "View your dashboard",
      "url": "/dashboard?source=pwa",
      "icons": [{ "src": "/icons/dashboard.png", "sizes": "96x96" }]
    }
  ],
  "share_target": {
    "action": "/share",
    "method": "POST",
    "enctype": "multipart/form-data",
    "params": {
      "title": "title",
      "text": "text",
      "url": "url",
      "files": [
        {
          "name": "media",
          "accept": ["image/*", "video/*"]
        }
      ]
    }
  },
  "protocol_handlers": [
    {
      "protocol": "web+myapp",
      "url": "/handle?url=%s"
    }
  ]
}

// Add to HTML head
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#007acc">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="My PWA">
<link rel="apple-touch-icon" href="/icons/icon-192x192.png">

Example: Install prompt handling with React

// useInstallPrompt.ts
import { useEffect, useState } from 'react';

interface BeforeInstallPromptEvent extends Event {
  prompt: () => Promise<void>;
  userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
}

export function useInstallPrompt() {
  const [installPrompt, setInstallPrompt] = useState<BeforeInstallPromptEvent | null>(null);
  const [isInstalled, setIsInstalled] = useState(false);

  useEffect(() => {
    const handleBeforeInstall = (e: Event) => {
      e.preventDefault();
      setInstallPrompt(e as BeforeInstallPromptEvent);
    };

    const handleAppInstalled = () => {
      setIsInstalled(true);
      setInstallPrompt(null);
    };

    window.addEventListener('beforeinstallprompt', handleBeforeInstall);
    window.addEventListener('appinstalled', handleAppInstalled);

    // Check if already installed
    if (window.matchMedia('(display-mode: standalone)').matches) {
      setIsInstalled(true);
    }

    return () => {
      window.removeEventListener('beforeinstallprompt', handleBeforeInstall);
      window.removeEventListener('appinstalled', handleAppInstalled);
    };
  }, []);

  const promptInstall = async () => {
    if (!installPrompt) return false;

    await installPrompt.prompt();
    const { outcome } = await installPrompt.userChoice;

    if (outcome === 'accepted') {
      setInstallPrompt(null);
      return true;
    }

    return false;
  };

  return { canInstall: !!installPrompt, isInstalled, promptInstall };
}

// Usage in component
function InstallButton() {
  const { canInstall, isInstalled, promptInstall } = useInstallPrompt();

  if (isInstalled) {
    return <p>✓ App installed</p>;
  }

  if (!canInstall) {
    return null;
  }

  return (
    <button onClick={promptInstall}>
      📱 Install App
    </button>
  );
}

Example: Vite PWA plugin setup

// Installation
npm install -D vite-plugin-pwa

// vite.config.ts
import { defineConfig } from 'vite';
import { VitePWA } from 'vite-plugin-pwa';

export default defineConfig({
  plugins: [
    VitePWA({
      registerType: 'autoUpdate',
      includeAssets: ['favicon.ico', 'robots.txt', 'apple-touch-icon.png'],
      manifest: {
        name: 'My PWA App',
        short_name: 'PWA',
        description: 'Progressive Web Application',
        theme_color: '#007acc',
        icons: [
          {
            src: '/icon-192.png',
            sizes: '192x192',
            type: 'image/png'
          },
          {
            src: '/icon-512.png',
            sizes: '512x512',
            type: 'image/png'
          }
        ]
      },
      workbox: {
        runtimeCaching: [
          {
            urlPattern: /^https:\/\/api\./,
            handler: 'NetworkFirst',
            options: {
              cacheName: 'api-cache',
              expiration: {
                maxEntries: 50,
                maxAgeSeconds: 300
              }
            }
          }
        ]
      }
    })
  ]
});
Installation Requirements: HTTPS required (except localhost). Service worker must be registered. Manifest must include name, icons (192x192, 512x512), and start_url. User engagement needed before install prompt.

3. Push Notifications Web Push

Implement push notifications with Web Push API and service workers for re-engagement, updates, and real-time alerts.

Component Purpose Technology Required
VAPID Keys Server authentication Public/private key pair Yes
Push Subscription User permission & endpoint PushManager API Yes
Service Worker Receive notifications Push event listener Yes
Push Server Send notifications web-push library Yes
Notification Display message Notification API Yes

Example: Frontend push notification setup

// usePushNotifications.ts
import { useState, useEffect } from 'react';

const VAPID_PUBLIC_KEY = 'YOUR_PUBLIC_VAPID_KEY';

function urlBase64ToUint8Array(base64String: string): Uint8Array {
  const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
  const base64 = (base64String + padding)
    .replace(/\-/g, '+')
    .replace(/_/g, '/');
  const rawData = window.atob(base64);
  return Uint8Array.from([...rawData].map(char => char.charCodeAt(0)));
}

export function usePushNotifications() {
  const [subscription, setSubscription] = useState<PushSubscription | null>(null);
  const [permission, setPermission] = useState<NotificationPermission>('default');

  useEffect(() => {
    if ('Notification' in window) {
      setPermission(Notification.permission);
    }
  }, []);

  const subscribe = async () => {
    if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
      throw new Error('Push notifications not supported');
    }

    // Request permission
    const permission = await Notification.requestPermission();
    setPermission(permission);

    if (permission !== 'granted') {
      throw new Error('Permission denied');
    }

    // Get service worker registration
    const registration = await navigator.serviceWorker.ready;

    // Subscribe to push notifications
    const sub = await registration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
    });

    setSubscription(sub);

    // Send subscription to backend
    await fetch('/api/push/subscribe', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(sub)
    });

    return sub;
  };

  const unsubscribe = async () => {
    if (!subscription) return;

    await subscription.unsubscribe();
    
    // Notify backend
    await fetch('/api/push/unsubscribe', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ endpoint: subscription.endpoint })
    });

    setSubscription(null);
  };

  return { subscription, permission, subscribe, unsubscribe };
}

// Usage in component
function NotificationSettings() {
  const { permission, subscribe, unsubscribe, subscription } = usePushNotifications();

  const handleEnable = async () => {
    try {
      await subscribe();
      alert('Notifications enabled!');
    } catch (error) {
      console.error('Failed to subscribe:', error);
    }
  };

  if (permission === 'denied') {
    return <p>Notifications blocked. Enable in browser settings.</p>;
  }

  return (
    <div>
      {subscription ? (
        <button onClick={unsubscribe}>Disable Notifications</button>
      ) : (
        <button onClick={handleEnable}>Enable Notifications</button>
      )}
    </div>
  );
}

Example: Service worker push event handler

// service-worker.js
self.addEventListener('push', (event) => {
  if (!event.data) return;

  const data = event.data.json();
  const { title, body, icon, badge, image, tag, actions, url } = data;

  const options = {
    body,
    icon: icon || '/icons/icon-192x192.png',
    badge: badge || '/icons/badge-72x72.png',
    image,
    tag: tag || 'default-tag',
    requireInteraction: false,
    vibrate: [200, 100, 200],
    data: { url: url || '/' },
    actions: actions || [
      { action: 'open', title: 'Open', icon: '/icons/open.png' },
      { action: 'close', title: 'Close', icon: '/icons/close.png' }
    ]
  };

  event.waitUntil(
    self.registration.showNotification(title, options)
  );
});

// Handle notification click
self.addEventListener('notificationclick', (event) => {
  event.notification.close();

  if (event.action === 'close') {
    return;
  }

  const url = event.notification.data?.url || '/';

  event.waitUntil(
    clients.matchAll({ type: 'window', includeUncontrolled: true })
      .then((clientList) => {
        // Check if window is already open
        for (const client of clientList) {
          if (client.url === url && 'focus' in client) {
            return client.focus();
          }
        }
        // Open new window
        if (clients.openWindow) {
          return clients.openWindow(url);
        }
      })
  );
});

// Handle notification close
self.addEventListener('notificationclose', (event) => {
  console.log('Notification closed:', event.notification.tag);
  
  // Track analytics
  fetch('/api/analytics/notification-close', {
    method: 'POST',
    body: JSON.stringify({ tag: event.notification.tag })
  });
});

Example: Backend push notification server (Node.js)

// Installation
npm install web-push

// Generate VAPID keys (run once)
const webpush = require('web-push');
const vapidKeys = webpush.generateVAPIDKeys();
console.log('Public Key:', vapidKeys.publicKey);
console.log('Private Key:', vapidKeys.privateKey);

// server.js
const express = require('express');
const webpush = require('web-push');

const app = express();
app.use(express.json());

// Configure VAPID keys
webpush.setVapidDetails(
  'mailto:your-email@example.com',
  process.env.VAPID_PUBLIC_KEY,
  process.env.VAPID_PRIVATE_KEY
);

// Store subscriptions (use database in production)
const subscriptions = new Map();

// Subscribe endpoint
app.post('/api/push/subscribe', (req, res) => {
  const subscription = req.body;
  subscriptions.set(subscription.endpoint, subscription);
  res.status(201).json({ success: true });
});

// Unsubscribe endpoint
app.post('/api/push/unsubscribe', (req, res) => {
  const { endpoint } = req.body;
  subscriptions.delete(endpoint);
  res.json({ success: true });
});

// Send notification
app.post('/api/push/send', async (req, res) => {
  const { title, body, url, userId } = req.body;

  const payload = JSON.stringify({
    title,
    body,
    icon: '/icons/icon-192x192.png',
    badge: '/icons/badge-72x72.png',
    url,
    actions: [
      { action: 'open', title: 'View' },
      { action: 'close', title: 'Dismiss' }
    ]
  });

  // Send to all subscriptions (or filter by userId)
  const promises = Array.from(subscriptions.values()).map(subscription =>
    webpush.sendNotification(subscription, payload)
      .catch(error => {
        // Handle expired subscriptions
        if (error.statusCode === 410) {
          subscriptions.delete(subscription.endpoint);
        }
        console.error('Push error:', error);
      })
  );

  await Promise.all(promises);
  res.json({ success: true, sent: promises.length });
});

app.listen(3000, () => console.log('Server running on port 3000'));
Best Practices: Always ask permission contextually, not immediately on page load. Provide clear value proposition. Allow easy unsubscribe. Use notification tags to avoid duplicates. Handle expired subscriptions gracefully.

4. Background Sync Offline Queue

Implement Background Sync API to defer actions until network connectivity is restored, ensuring reliable data submission.

Feature Purpose Use Case Browser Support
Background Sync Retry failed requests Form submissions, messages Chrome, Edge
Periodic Sync Regular background updates News, content refresh Limited support
SyncManager Register sync events Sync orchestration Chrome, Edge

Example: Background Sync for offline form submission

// useBackgroundSync.ts
import { useState } from 'react';

export function useBackgroundSync() {
  const [pending, setPending] = useState<number>(0);

  const queueSync = async (tag: string, data: any) => {
    if (!('serviceWorker' in navigator) || !('SyncManager' in window)) {
      // Fallback: immediate submission
      return fetch('/api/submit', {
        method: 'POST',
        body: JSON.stringify(data),
        headers: { 'Content-Type': 'application/json' }
      });
    }

    // Store data in IndexedDB
    const db = await openDatabase();
    await db.add('sync-queue', { tag, data, timestamp: Date.now() });

    // Register background sync
    const registration = await navigator.serviceWorker.ready;
    await registration.sync.register(tag);

    setPending(prev => prev + 1);
  };

  return { queueSync, pending };
}

// service-worker.js
self.addEventListener('sync', (event) => {
  if (event.tag.startsWith('form-submit-')) {
    event.waitUntil(syncFormData(event.tag));
  } else if (event.tag === 'message-queue') {
    event.waitUntil(syncMessages());
  }
});

async function syncFormData(tag) {
  const db = await openDatabase();
  const items = await db.getAllFromIndex('sync-queue', 'by-tag', tag);

  for (const item of items) {
    try {
      const response = await fetch('/api/submit', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(item.data)
      });

      if (response.ok) {
        await db.delete('sync-queue', item.id);
        
        // Notify clients
        const clients = await self.clients.matchAll();
        clients.forEach(client => {
          client.postMessage({
            type: 'SYNC_SUCCESS',
            tag: item.tag
          });
        });
      } else {
        throw new Error('Server error');
      }
    } catch (error) {
      console.error('Sync failed:', error);
      // Will retry automatically
    }
  }
}

async function syncMessages() {
  const db = await openDatabase();
  const messages = await db.getAll('message-queue');

  for (const message of messages) {
    try {
      await fetch('/api/messages', {
        method: 'POST',
        body: JSON.stringify(message),
        headers: { 'Content-Type': 'application/json' }
      });

      await db.delete('message-queue', message.id);
    } catch (error) {
      console.error('Message sync failed:', error);
    }
  }
}

Example: Offline-first form component

// OfflineForm.tsx
import { useState } from 'react';
import { useBackgroundSync } from './useBackgroundSync';

export function OfflineForm() {
  const [formData, setFormData] = useState({ name: '', email: '', message: '' });
  const [status, setStatus] = useState<'idle' | 'submitting' | 'queued'>('idle');
  const { queueSync } = useBackgroundSync();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setStatus('submitting');

    try {
      if (navigator.onLine) {
        // Online: submit immediately
        const response = await fetch('/api/contact', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(formData)
        });

        if (response.ok) {
          alert('Form submitted successfully!');
          setFormData({ name: '', email: '', message: '' });
        }
      } else {
        // Offline: queue for background sync
        await queueSync('form-submit-contact', formData);
        setStatus('queued');
        alert('You are offline. Form will be submitted when connection is restored.');
      }
    } catch (error) {
      // Network error: queue for sync
      await queueSync('form-submit-contact', formData);
      setStatus('queued');
      alert('Submission queued. Will retry automatically.');
    } finally {
      setStatus('idle');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={formData.name}
        onChange={e => setFormData({ ...formData, name: e.target.value })}
        placeholder="Name"
        required
      />
      <input
        type="email"
        value={formData.email}
        onChange={e => setFormData({ ...formData, email: e.target.value })}
        placeholder="Email"
        required
      />
      <textarea
        value={formData.message}
        onChange={e => setFormData({ ...formData, message: e.target.value })}
        placeholder="Message"
        required
      />
      <button type="submit" disabled={status === 'submitting'}>
        {status === 'submitting' ? 'Submitting...' : 
         status === 'queued' ? 'Queued for Sync' : 
         'Submit'}
      </button>
    </form>
  );
}

Example: Periodic Background Sync (Chrome only)

// Register periodic sync (requires user engagement)
async function registerPeriodicSync() {
  const registration = await navigator.serviceWorker.ready;

  if ('periodicSync' in registration) {
    try {
      await registration.periodicSync.register('content-sync', {
        minInterval: 24 * 60 * 60 * 1000 // 24 hours
      });
      console.log('Periodic sync registered');
    } catch (error) {
      console.error('Periodic sync registration failed:', error);
    }
  }
}

// service-worker.js
self.addEventListener('periodicsync', (event) => {
  if (event.tag === 'content-sync') {
    event.waitUntil(syncContent());
  }
});

async function syncContent() {
  try {
    // Fetch latest content
    const response = await fetch('/api/content/latest');
    const content = await response.json();

    // Update cache
    const cache = await caches.open('content-cache');
    await cache.put('/api/content/latest', new Response(JSON.stringify(content)));

    // Notify clients
    const clients = await self.clients.matchAll();
    clients.forEach(client => {
      client.postMessage({
        type: 'CONTENT_UPDATED',
        count: content.length
      });
    });
  } catch (error) {
    console.error('Periodic sync failed:', error);
  }
}

// Unregister periodic sync
async function unregisterPeriodicSync() {
  const registration = await navigator.serviceWorker.ready;
  if ('periodicSync' in registration) {
    await registration.periodicSync.unregister('content-sync');
  }
}
Browser Support: Background Sync supported in Chrome, Edge, Opera. Safari and Firefox have limited/no support. Always implement fallback for immediate submission. Test offline scenarios thoroughly.

5. IndexedDB Storage Management

Use IndexedDB for client-side structured data storage with queries, indexes, and transactions for offline-first apps.

Library Features API Style Bundle Size
idb (Jake Archibald) Promise wrapper, simple API Modern async/await ~1KB
Dexie.js Rich queries, relationships Declarative ~20KB
LocalForage localStorage-like API Simple key-value ~8KB
PouchDB CouchDB sync, replication Document database ~140KB
// Installation
npm install idb

// db.ts - Database setup
import { openDB, DBSchema, IDBPDatabase } from 'idb';

interface MyDB extends DBSchema {
  products: {
    key: string;
    value: {
      id: string;
      name: string;
      price: number;
      category: string;
      createdAt: Date;
    };
    indexes: { 'by-category': string; 'by-price': number };
  };
  cart: {
    key: string;
    value: {
      productId: string;
      quantity: number;
      addedAt: Date;
    };
  };
  syncQueue: {
    key: number;
    value: {
      action: string;
      data: any;
      timestamp: number;
    };
  };
}

let dbPromise: Promise<IDBPDatabase<MyDB>> | null = null;

export function getDB() {
  if (!dbPromise) {
    dbPromise = openDB<MyDB>('my-pwa-db', 1, {
      upgrade(db) {
        // Create products store
        const productStore = db.createObjectStore('products', { keyPath: 'id' });
        productStore.createIndex('by-category', 'category');
        productStore.createIndex('by-price', 'price');

        // Create cart store
        db.createObjectStore('cart', { keyPath: 'productId' });

        // Create sync queue store
        db.createObjectStore('syncQueue', { keyPath: 'id', autoIncrement: true });
      }
    });
  }
  return dbPromise;
}

// products.ts - CRUD operations
export async function addProduct(product: MyDB['products']['value']) {
  const db = await getDB();
  await db.add('products', product);
}

export async function getProduct(id: string) {
  const db = await getDB();
  return db.get('products', id);
}

export async function getAllProducts() {
  const db = await getDB();
  return db.getAll('products');
}

export async function getProductsByCategory(category: string) {
  const db = await getDB();
  return db.getAllFromIndex('products', 'by-category', category);
}

export async function updateProduct(product: MyDB['products']['value']) {
  const db = await getDB();
  await db.put('products', product);
}

export async function deleteProduct(id: string) {
  const db = await getDB();
  await db.delete('products', id);
}

// Advanced queries with cursor
export async function getProductsInPriceRange(min: number, max: number) {
  const db = await getDB();
  const range = IDBKeyRange.bound(min, max);
  return db.getAllFromIndex('products', 'by-price', range);
}

// Bulk operations with transaction
export async function bulkAddProducts(products: MyDB['products']['value'][]) {
  const db = await getDB();
  const tx = db.transaction('products', 'readwrite');
  
  await Promise.all([
    ...products.map(product => tx.store.add(product)),
    tx.done
  ]);
}

Example: React hook for IndexedDB

// useIndexedDB.ts
import { useState, useEffect } from 'react';
import { getDB } from './db';
import type { DBSchema } from 'idb';

export function useIndexedDB<T>(storeName: string) {
  const [data, setData] = useState<T[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    loadData();
  }, [storeName]);

  const loadData = async () => {
    try {
      setLoading(true);
      const db = await getDB();
      const items = await db.getAll(storeName as any);
      setData(items as T[]);
      setError(null);
    } catch (err) {
      setError(err as Error);
    } finally {
      setLoading(false);
    }
  };

  const add = async (item: T) => {
    try {
      const db = await getDB();
      await db.add(storeName as any, item);
      await loadData();
    } catch (err) {
      setError(err as Error);
    }
  };

  const update = async (item: T) => {
    try {
      const db = await getDB();
      await db.put(storeName as any, item);
      await loadData();
    } catch (err) {
      setError(err as Error);
    }
  };

  const remove = async (key: string | number) => {
    try {
      const db = await getDB();
      await db.delete(storeName as any, key);
      await loadData();
    } catch (err) {
      setError(err as Error);
    }
  };

  const clear = async () => {
    try {
      const db = await getDB();
      await db.clear(storeName as any);
      setData([]);
    } catch (err) {
      setError(err as Error);
    }
  };

  return { data, loading, error, add, update, remove, clear, refresh: loadData };
}

// Usage in component
function ProductList() {
  const { data: products, loading, add, remove } = useIndexedDB<Product>('products');

  if (loading) return <div>Loading...</div>;

  return (
    <div>
      <h2>Products ({products.length})</h2>
      <ul>
        {products.map(product => (
          <li key={product.id}>
            {product.name} - ${product.price}
            <button onClick={() => remove(product.id)}>Delete</button>
          </li>
        ))}
      </ul>
      <button onClick={() => add({ id: Date.now().toString(), name: 'New Product', price: 99 })}>
        Add Product
      </button>
    </div>
  );
}

Example: Dexie.js for advanced queries

// Installation
npm install dexie

// db.ts
import Dexie, { Table } from 'dexie';

interface Product {
  id?: number;
  name: string;
  price: number;
  category: string;
  tags: string[];
  inStock: boolean;
}

class MyDatabase extends Dexie {
  products!: Table<Product>;

  constructor() {
    super('MyAppDB');
    this.version(1).stores({
      products: '++id, name, price, category, *tags, inStock'
      // ++ = auto-increment
      // * = multi-entry index (for arrays)
    });
  }
}

export const db = new MyDatabase();

// Complex queries
export async function searchProducts(query: string) {
  return db.products
    .where('name')
    .startsWithIgnoreCase(query)
    .or('category')
    .equalsIgnoreCase(query)
    .toArray();
}

export async function getInStockProducts() {
  return db.products.where('inStock').equals(1).toArray();
}

export async function getProductsByTags(tags: string[]) {
  return db.products.where('tags').anyOf(tags).toArray();
}

export async function getAffordableProducts(maxPrice: number) {
  return db.products.where('price').below(maxPrice).toArray();
}

// Bulk operations
export async function bulkUpdate() {
  await db.products.where('category').equals('Electronics').modify({
    inStock: false
  });
}

// Transactions
export async function transferStock(fromProductId: number, toProductId: number) {
  await db.transaction('rw', db.products, async () => {
    const fromProduct = await db.products.get(fromProductId);
    const toProduct = await db.products.get(toProductId);

    if (fromProduct && toProduct) {
      await db.products.update(fromProductId, { inStock: false });
      await db.products.update(toProductId, { inStock: true });
    }
  });
}

// Hooks integration
import { useLiveQuery } from 'dexie-react-hooks';

function ProductList() {
  const products = useLiveQuery(() => db.products.toArray());

  if (!products) return <div>Loading...</div>;

  return (
    <ul>
      {products.map(p => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  );
}
Storage Limits: IndexedDB typically allows 50% of available disk space. Chrome/Edge: ~60% of available storage. Firefox: ~50%. Monitor quota with navigator.storage.estimate(). Implement cleanup strategies for old data.

6. App Shell Architecture Performance

Implement App Shell pattern for instant loading with cached static shell and dynamic content for optimal perceived performance.

Component Caching Strategy Priority Update Frequency
App Shell (HTML/CSS/JS) Cache First, pre-cache Critical On deployment
Static Assets Cache First High On version change
Dynamic Content Network First Medium Real-time
API Responses Stale While Revalidate Medium Background
User Data IndexedDB High On change

Example: App Shell structure with React

// App shell components (always cached)
// AppShell.tsx
import { Suspense, lazy } from 'react';
import { Header } from './Header'; // Pre-cached
import { Sidebar } from './Sidebar'; // Pre-cached
import { Footer } from './Footer'; // Pre-cached

// Dynamic content (loaded separately)
const DynamicContent = lazy(() => import('./DynamicContent'));

export function AppShell() {
  return (
    <div className="app-shell">
      <Header />
      
      <div className="main-content">
        <Sidebar />
        
        <main>
          <Suspense fallback={<SkeletonLoader />}>
            <DynamicContent />
          </Suspense>
        </main>
      </div>
      
      <Footer />
    </div>
  );
}

// index.html - Minimal critical HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>My PWA</title>
  <link rel="manifest" href="/manifest.json">
  
  <!-- Inline critical CSS for instant render -->
  <style>
    .app-shell { display: flex; flex-direction: column; min-height: 100vh; }
    .skeleton { background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); }
    /* ... minimal styles ... */
  </style>
</head>
<body>
  <div id="root">
    <!-- Initial shell renders immediately from cache -->
    <div class="app-shell">
      <header class="skeleton"></header>
      <main class="skeleton"></main>
      <footer class="skeleton"></footer>
    </div>
  </div>
  <script type="module" src="/src/main.tsx"></script>
</body>
</html>

Example: Service worker for App Shell caching

// service-worker.js with Workbox
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute, NavigationRoute } from 'workbox-routing';
import { NetworkFirst, CacheFirst, StaleWhileRevalidate } from 'workbox-strategies';

// Pre-cache app shell
const APP_SHELL_CACHE = 'app-shell-v1';
const appShellFiles = [
  '/',
  '/index.html',
  '/styles/main.css',
  '/scripts/app.js',
  '/offline.html'
];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(APP_SHELL_CACHE).then((cache) => {
      return cache.addAll(appShellFiles);
    })
  );
  self.skipWaiting();
});

// Serve app shell for all navigation requests
registerRoute(
  new NavigationRoute(
    new NetworkFirst({
      cacheName: APP_SHELL_CACHE,
      plugins: [
        {
          cacheWillUpdate: async ({ response }) => {
            // Only cache successful responses
            return response.status === 200 ? response : null;
          }
        }
      ]
    })
  )
);

// Cache static assets
registerRoute(
  ({ request }) => request.destination === 'style' || 
                    request.destination === 'script' ||
                    request.destination === 'font',
  new CacheFirst({
    cacheName: 'static-assets-v1',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 60,
        maxAgeSeconds: 30 * 24 * 60 * 60 // 30 days
      })
    ]
  })
);

// Dynamic content with network priority
registerRoute(
  ({ url }) => url.pathname.startsWith('/api/'),
  new NetworkFirst({
    cacheName: 'api-cache-v1',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 50,
        maxAgeSeconds: 5 * 60 // 5 minutes
      })
    ]
  })
);

// Images with cache priority
registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'image-cache-v1',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 100,
        maxAgeSeconds: 7 * 24 * 60 * 60 // 7 days
      })
    ]
  })
);

Example: Performance optimization with App Shell

// SkeletonLoader.tsx - Immediate UI feedback
export function SkeletonLoader() {
  return (
    <div className="skeleton-container">
      <div className="skeleton skeleton-text" style={{ width: '60%' }} />
      <div className="skeleton skeleton-text" style={{ width: '80%' }} />
      <div className="skeleton skeleton-text" style={{ width: '70%' }} />
      <div className="skeleton skeleton-card" />
    </div>
  );
}

// Performance monitoring
export function measureAppShellPerformance() {
  if ('performance' in window) {
    window.addEventListener('load', () => {
      const perfData = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
      
      const metrics = {
        // Time to First Byte
        ttfb: perfData.responseStart - perfData.requestStart,
        
        // DOM Content Loaded
        domContentLoaded: perfData.domContentLoadedEventEnd - perfData.domContentLoadedEventStart,
        
        // Full page load
        loadComplete: perfData.loadEventEnd - perfData.loadEventStart,
        
        // App shell render (custom)
        appShellRender: performance.now()
      };
      
      // Send to analytics
      console.log('App Shell Performance:', metrics);
      
      // Report to backend
      navigator.sendBeacon('/api/analytics/performance', JSON.stringify(metrics));
    });
  }
}

// useAppShellCache.ts - React hook
import { useEffect, useState } from 'react';

export function useAppShellCache() {
  const [cacheStatus, setCacheStatus] = useState<'checking' | 'cached' | 'error'>('checking');

  useEffect(() => {
    checkAppShellCache();
  }, []);

  const checkAppShellCache = async () => {
    try {
      const cache = await caches.open('app-shell-v1');
      const cachedRequests = await cache.keys();
      
      if (cachedRequests.length > 0) {
        setCacheStatus('cached');
      } else {
        setCacheStatus('error');
      }
    } catch (error) {
      setCacheStatus('error');
    }
  };

  const clearCache = async () => {
    await caches.delete('app-shell-v1');
    window.location.reload();
  };

  return { cacheStatus, clearCache };
}

PWA Implementation Checklist

Component Implementation Testing Impact
Service Worker Workbox with caching strategies DevTools offline mode Offline functionality
Web Manifest Complete manifest.json, install prompt Lighthouse PWA audit Installability
Push Notifications VAPID keys, push subscription, handlers Push notification test Re-engagement
Background Sync SyncManager, offline queue Offline form submit test Reliability
IndexedDB idb/Dexie for structured storage Storage quota checks Data persistence
App Shell Pre-cached shell, dynamic content Performance metrics Fast loading