JavaScript PWA

JavaScript PWA Development: Service Workers, Offline Support, and App-like Experiences

Build Progressive Web Apps with JavaScript. Master service workers, offline functionality, push notifications, and native app-like experiences.

By JavaScript Document Team
pwaservice-workersofflinepush-notificationsapp-manifestweb-apps

Progressive Web Apps (PWAs) combine the best of web and mobile applications, providing app-like experiences through modern web technologies. Using service workers, web app manifests, and advanced caching strategies, PWAs deliver offline functionality, push notifications, and native app-like performance. This comprehensive guide covers building production-ready PWAs with JavaScript.

PWA Fundamentals and Setup

Service Worker Registration and Management

// PWA Manager for comprehensive PWA functionality
class PWAManager {
  constructor() {
    this.serviceWorker = null;
    this.registration = null;
    this.isOnline = navigator.onLine;
    this.installPromptEvent = null;
    this.updateAvailable = false;

    this.setupEventListeners();
  }

  // Initialize PWA
  async initialize() {
    try {
      // Check PWA support
      this.checkPWASupport();

      // Register service worker
      await this.registerServiceWorker();

      // Setup install prompt
      this.setupInstallPrompt();

      // Setup network monitoring
      this.setupNetworkMonitoring();

      console.log('PWA initialized successfully');
      return true;
    } catch (error) {
      console.error('PWA initialization failed:', error);
      return false;
    }
  }

  // Check PWA support
  checkPWASupport() {
    const support = {
      serviceWorker: 'serviceWorker' in navigator,
      pushNotifications: 'PushManager' in window,
      notifications: 'Notification' in window,
      indexedDB: 'indexedDB' in window,
      cache: 'caches' in window,
      backgroundSync:
        'serviceWorker' in navigator &&
        'sync' in window.ServiceWorkerRegistration.prototype,
    };

    console.log('PWA Support:', support);
    return support;
  }

  // Register service worker
  async registerServiceWorker(swPath = '/sw.js') {
    if (!('serviceWorker' in navigator)) {
      throw new Error('Service workers not supported');
    }

    try {
      this.registration = await navigator.serviceWorker.register(swPath);

      console.log('Service Worker registered:', this.registration);

      // Handle updates
      this.registration.addEventListener('updatefound', () => {
        const newWorker = this.registration.installing;
        newWorker.addEventListener('statechange', () => {
          if (newWorker.state === 'installed') {
            if (navigator.serviceWorker.controller) {
              // New version available
              this.updateAvailable = true;
              this.onServiceWorkerUpdate();
            } else {
              // First time install
              this.onServiceWorkerInstall();
            }
          }
        });
      });

      return this.registration;
    } catch (error) {
      console.error('Service Worker registration failed:', error);
      throw error;
    }
  }

  // Setup install prompt
  setupInstallPrompt() {
    window.addEventListener('beforeinstallprompt', (event) => {
      event.preventDefault();
      this.installPromptEvent = event;
      this.onInstallPromptReady();
    });

    window.addEventListener('appinstalled', () => {
      this.installPromptEvent = null;
      this.onAppInstalled();
    });
  }

  // Setup network monitoring
  setupNetworkMonitoring() {
    window.addEventListener('online', () => {
      this.isOnline = true;
      this.onNetworkStatusChange(true);
    });

    window.addEventListener('offline', () => {
      this.isOnline = false;
      this.onNetworkStatusChange(false);
    });
  }

  // Setup general event listeners
  setupEventListeners() {
    // Listen for messages from service worker
    navigator.serviceWorker?.addEventListener('message', (event) => {
      this.handleServiceWorkerMessage(event.data);
    });
  }

  // Show install prompt
  async showInstallPrompt() {
    if (!this.installPromptEvent) {
      throw new Error('Install prompt not available');
    }

    const result = await this.installPromptEvent.prompt();
    console.log('Install prompt result:', result);

    this.installPromptEvent = null;
    return result;
  }

  // Check if app is installed
  isAppInstalled() {
    return (
      window.matchMedia('(display-mode: standalone)').matches ||
      window.navigator.standalone === true
    );
  }

  // Send message to service worker
  async sendMessageToSW(message) {
    if (!this.registration?.active) {
      throw new Error('No active service worker');
    }

    return new Promise((resolve) => {
      const messageChannel = new MessageChannel();
      messageChannel.port1.onmessage = (event) => {
        resolve(event.data);
      };

      this.registration.active.postMessage(message, [messageChannel.port2]);
    });
  }

  // Update service worker
  async updateServiceWorker() {
    if (!this.registration) return;

    await this.registration.update();
  }

  // Skip waiting for new service worker
  async skipWaiting() {
    await this.sendMessageToSW({ action: 'skipWaiting' });
  }

  // Event handlers (override in subclasses)
  onServiceWorkerInstall() {
    console.log('Service Worker installed for the first time');
  }

  onServiceWorkerUpdate() {
    console.log('New service worker available');
    this.showUpdateNotification();
  }

  onInstallPromptReady() {
    console.log('App can be installed');
    this.showInstallButton();
  }

  onAppInstalled() {
    console.log('App installed successfully');
    this.showSuccessMessage('App installed successfully!');
  }

  onNetworkStatusChange(isOnline) {
    console.log('Network status changed:', isOnline ? 'online' : 'offline');
    this.updateOfflineIndicator(!isOnline);
  }

  handleServiceWorkerMessage(data) {
    console.log('Message from service worker:', data);

    switch (data.type) {
      case 'CACHE_UPDATED':
        this.onCacheUpdated(data.payload);
        break;
      case 'SYNC_COMPLETE':
        this.onSyncComplete(data.payload);
        break;
      case 'PUSH_RECEIVED':
        this.onPushReceived(data.payload);
        break;
    }
  }

  onCacheUpdated(payload) {
    console.log('Cache updated:', payload);
  }

  onSyncComplete(payload) {
    console.log('Background sync complete:', payload);
  }

  onPushReceived(payload) {
    console.log('Push notification received:', payload);
  }

  // UI Helper methods
  showUpdateNotification() {
    const notification = this.createNotification(
      'Update Available',
      'A new version of the app is ready. Click to update.',
      [
        {
          text: 'Update Now',
          action: () => this.skipWaiting().then(() => location.reload()),
        },
        { text: 'Later', action: () => notification.remove() },
      ]
    );
  }

  showInstallButton() {
    if (document.getElementById('pwa-install-btn')) return;

    const button = document.createElement('button');
    button.id = 'pwa-install-btn';
    button.textContent = 'Install App';
    button.style.cssText = `
      position: fixed;
      bottom: 20px;
      right: 20px;
      background: #007bff;
      color: white;
      border: none;
      padding: 12px 24px;
      border-radius: 25px;
      cursor: pointer;
      z-index: 1000;
      box-shadow: 0 4px 12px rgba(0,0,0,0.2);
    `;

    button.addEventListener('click', () => {
      this.showInstallPrompt();
      button.remove();
    });

    document.body.appendChild(button);
  }

  createNotification(title, message, actions = []) {
    const notification = document.createElement('div');
    notification.style.cssText = `
      position: fixed;
      top: 20px;
      right: 20px;
      background: white;
      border: 1px solid #ddd;
      border-radius: 8px;
      padding: 16px;
      box-shadow: 0 4px 12px rgba(0,0,0,0.2);
      z-index: 10000;
      max-width: 350px;
    `;

    const actionsHTML = actions
      .map(
        (action, index) =>
          `<button class="action-btn" data-index="${index}" style="
        background: ${index === 0 ? '#007bff' : 'transparent'};
        color: ${index === 0 ? 'white' : '#007bff'};
        border: 1px solid #007bff;
        padding: 6px 12px;
        border-radius: 4px;
        margin-right: 8px;
        cursor: pointer;
      ">${action.text}</button>`
      )
      .join('');

    notification.innerHTML = `
      <h4 style="margin: 0 0 8px 0;">${title}</h4>
      <p style="margin: 0 0 12px 0; color: #666;">${message}</p>
      <div>${actionsHTML}</div>
    `;

    // Add action handlers
    notification.addEventListener('click', (e) => {
      if (e.target.classList.contains('action-btn')) {
        const index = parseInt(e.target.dataset.index);
        actions[index].action();
      }
    });

    document.body.appendChild(notification);
    return notification;
  }

  updateOfflineIndicator(isOffline) {
    let indicator = document.getElementById('offline-indicator');

    if (isOffline && !indicator) {
      indicator = document.createElement('div');
      indicator.id = 'offline-indicator';
      indicator.textContent = 'You are offline';
      indicator.style.cssText = `
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        background: #f39c12;
        color: white;
        text-align: center;
        padding: 8px;
        z-index: 9999;
      `;
      document.body.appendChild(indicator);
    } else if (!isOffline && indicator) {
      indicator.remove();
    }
  }

  showSuccessMessage(message) {
    const toast = document.createElement('div');
    toast.textContent = message;
    toast.style.cssText = `
      position: fixed;
      bottom: 20px;
      left: 50%;
      transform: translateX(-50%);
      background: #28a745;
      color: white;
      padding: 12px 24px;
      border-radius: 4px;
      z-index: 10000;
    `;

    document.body.appendChild(toast);

    setTimeout(() => {
      toast.remove();
    }, 3000);
  }
}

// Cache Manager for advanced caching strategies
class CacheManager {
  constructor() {
    this.strategies = {
      CACHE_FIRST: 'cache-first',
      NETWORK_FIRST: 'network-first',
      CACHE_ONLY: 'cache-only',
      NETWORK_ONLY: 'network-only',
      STALE_WHILE_REVALIDATE: 'stale-while-revalidate',
    };
  }

  // Cache First Strategy
  async cacheFirst(request, cacheName = 'dynamic-cache') {
    const cache = await caches.open(cacheName);
    const cachedResponse = await cache.match(request);

    if (cachedResponse) {
      return cachedResponse;
    }

    try {
      const networkResponse = await fetch(request);
      if (networkResponse.ok) {
        cache.put(request, networkResponse.clone());
      }
      return networkResponse;
    } catch (error) {
      console.error('Network request failed:', error);
      throw error;
    }
  }

  // Network First Strategy
  async networkFirst(request, cacheName = 'dynamic-cache') {
    const cache = await caches.open(cacheName);

    try {
      const networkResponse = await fetch(request);
      if (networkResponse.ok) {
        cache.put(request, networkResponse.clone());
      }
      return networkResponse;
    } catch (error) {
      console.warn('Network failed, trying cache:', error);
      const cachedResponse = await cache.match(request);
      if (cachedResponse) {
        return cachedResponse;
      }
      throw error;
    }
  }

  // Stale While Revalidate Strategy
  async staleWhileRevalidate(request, cacheName = 'dynamic-cache') {
    const cache = await caches.open(cacheName);
    const cachedResponse = await cache.match(request);

    // Start network request in background
    const networkPromise = fetch(request)
      .then((response) => {
        if (response.ok) {
          cache.put(request, response.clone());
        }
        return response;
      })
      .catch((error) => {
        console.warn('Background network request failed:', error);
      });

    // Return cached response immediately if available
    if (cachedResponse) {
      return cachedResponse;
    }

    // Wait for network if no cache
    return networkPromise;
  }

  // Cache with expiration
  async cacheWithExpiration(request, response, cacheName, maxAge = 86400000) {
    const cache = await caches.open(cacheName);

    // Add timestamp to response headers
    const responseToCache = new Response(response.body, {
      status: response.status,
      statusText: response.statusText,
      headers: {
        ...Object.fromEntries(response.headers.entries()),
        'sw-cache-timestamp': Date.now().toString(),
        'sw-cache-max-age': maxAge.toString(),
      },
    });

    await cache.put(request, responseToCache);
  }

  // Check if cached response is expired
  async isCacheExpired(cachedResponse, maxAge = 86400000) {
    const timestamp = cachedResponse.headers.get('sw-cache-timestamp');
    const cacheMaxAge = cachedResponse.headers.get('sw-cache-max-age');

    if (!timestamp) return false;

    const age = Date.now() - parseInt(timestamp);
    const expireTime = parseInt(cacheMaxAge) || maxAge;

    return age > expireTime;
  }

  // Clean expired cache entries
  async cleanExpiredEntries(cacheName) {
    const cache = await caches.open(cacheName);
    const requests = await cache.keys();

    for (const request of requests) {
      const response = await cache.match(request);
      if (response && (await this.isCacheExpired(response))) {
        await cache.delete(request);
      }
    }
  }

  // Get cache size
  async getCacheSize(cacheName) {
    const cache = await caches.open(cacheName);
    const requests = await cache.keys();

    let totalSize = 0;
    for (const request of requests) {
      const response = await cache.match(request);
      if (response) {
        const blob = await response.blob();
        totalSize += blob.size;
      }
    }

    return totalSize;
  }

  // Clear cache
  async clearCache(cacheName) {
    return await caches.delete(cacheName);
  }
}

// Generate web app manifest
function generateManifest(config) {
  const defaultConfig = {
    name: 'My Progressive Web App',
    short_name: 'MyPWA',
    description: 'A Progressive Web App built with JavaScript',
    start_url: '/',
    display: 'standalone',
    background_color: '#ffffff',
    theme_color: '#000000',
    orientation: 'portrait-primary',
    scope: '/',
    lang: 'en',
    dir: 'ltr',
  };

  const manifest = { ...defaultConfig, ...config };

  // Add icons if provided
  if (config.icons) {
    manifest.icons = config.icons.map((icon) => ({
      src: icon.src,
      sizes: icon.sizes || '192x192',
      type: icon.type || 'image/png',
      purpose: icon.purpose || 'any maskable',
    }));
  }

  // Add shortcuts if provided
  if (config.shortcuts) {
    manifest.shortcuts = config.shortcuts.map((shortcut) => ({
      name: shortcut.name,
      short_name: shortcut.short_name || shortcut.name,
      description: shortcut.description,
      url: shortcut.url,
      icons: shortcut.icons || [],
    }));
  }

  return manifest;
}

// Usage example
const manifest = generateManifest({
  name: 'JavaScript PWA Example',
  short_name: 'JS PWA',
  description: 'A comprehensive Progressive Web App example',
  theme_color: '#2196F3',
  background_color: '#ffffff',
  icons: [
    {
      src: '/images/icon-192.png',
      sizes: '192x192',
      type: 'image/png',
    },
    {
      src: '/images/icon-512.png',
      sizes: '512x512',
      type: 'image/png',
    },
  ],
  shortcuts: [
    {
      name: 'Quick Action',
      url: '/quick',
      description: 'Perform a quick action',
    },
  ],
});

console.log('Generated manifest:', manifest);

// Initialize PWA
const pwaManager = new PWAManager();
window.pwaManager = pwaManager; // Make globally available

pwaManager.initialize().then((success) => {
  if (success) {
    console.log('PWA ready!');
  } else {
    console.log('PWA initialization failed');
  }
});

Push Notifications and Offline Storage

Advanced Notification System

// Notification Manager for push notifications
class NotificationManager {
  constructor() {
    this.permission = Notification.permission;
    this.subscription = null;
    this.vapidPublicKey = null;
    this.serverEndpoint = '/api/notifications';
  }

  // Initialize notifications
  async initialize(vapidPublicKey = null) {
    this.vapidPublicKey = vapidPublicKey;

    // Check notification support
    if (!('Notification' in window)) {
      throw new Error('Notifications not supported');
    }

    // Request permission if needed
    if (this.permission === 'default') {
      await this.requestPermission();
    }

    // Subscribe to push notifications if permission granted
    if (this.permission === 'granted') {
      await this.subscribeToPush();
    }

    return this.permission === 'granted';
  }

  // Request notification permission
  async requestPermission() {
    const permission = await Notification.requestPermission();
    this.permission = permission;

    if (permission === 'granted') {
      console.log('Notification permission granted');
    } else if (permission === 'denied') {
      console.log('Notification permission denied');
    } else {
      console.log('Notification permission dismissed');
    }

    return permission;
  }

  // Subscribe to push notifications
  async subscribeToPush() {
    if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
      throw new Error('Push notifications not supported');
    }

    const registration = await navigator.serviceWorker.ready;

    try {
      this.subscription = await registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: this.vapidPublicKey
          ? this.urlBase64ToUint8Array(this.vapidPublicKey)
          : undefined,
      });

      console.log('Push subscription successful:', this.subscription);

      // Send subscription to server
      await this.sendSubscriptionToServer();

      return this.subscription;
    } catch (error) {
      console.error('Push subscription failed:', error);
      throw error;
    }
  }

  // Send subscription to server
  async sendSubscriptionToServer() {
    if (!this.subscription) {
      throw new Error('No subscription available');
    }

    const subscriptionData = {
      endpoint: this.subscription.endpoint,
      keys: {
        p256dh: this.arrayBufferToBase64(this.subscription.getKey('p256dh')),
        auth: this.arrayBufferToBase64(this.subscription.getKey('auth')),
      },
      userAgent: navigator.userAgent,
      timestamp: Date.now(),
    };

    try {
      const response = await fetch(`${this.serverEndpoint}/subscribe`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(subscriptionData),
      });

      if (!response.ok) {
        throw new Error(`Server responded with ${response.status}`);
      }

      console.log('Subscription sent to server');
    } catch (error) {
      console.error('Failed to send subscription to server:', error);
      throw error;
    }
  }

  // Show local notification
  showNotification(title, options = {}) {
    if (this.permission !== 'granted') {
      console.warn('Notification permission not granted');
      return null;
    }

    const defaultOptions = {
      body: '',
      icon: '/images/icon-192.png',
      badge: '/images/badge.png',
      vibrate: [100, 50, 100],
      requireInteraction: false,
      silent: false,
      tag: 'default',
      timestamp: Date.now(),
      data: {},
      actions: [],
    };

    const notificationOptions = { ...defaultOptions, ...options };

    return new Notification(title, notificationOptions);
  }

  // Create rich notification with actions
  createRichNotification(title, body, actions = []) {
    const defaultActions = [
      {
        action: 'view',
        title: 'View',
        icon: '/images/view-icon.png',
      },
      {
        action: 'dismiss',
        title: 'Dismiss',
        icon: '/images/dismiss-icon.png',
      },
    ];

    return this.showNotification(title, {
      body,
      actions: actions.length > 0 ? actions : defaultActions,
      requireInteraction: true,
      data: {
        timestamp: Date.now(),
        url: window.location.href,
      },
    });
  }

  // Utility functions
  urlBase64ToUint8Array(base64String) {
    const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
    const base64 = (base64String + padding)
      .replace(/-/g, '+')
      .replace(/_/g, '/');

    const rawData = window.atob(base64);
    const outputArray = new Uint8Array(rawData.length);

    for (let i = 0; i < rawData.length; ++i) {
      outputArray[i] = rawData.charCodeAt(i);
    }

    return outputArray;
  }

  arrayBufferToBase64(buffer) {
    const bytes = new Uint8Array(buffer);
    let binary = '';

    for (let i = 0; i < bytes.byteLength; i++) {
      binary += String.fromCharCode(bytes[i]);
    }

    return window.btoa(binary);
  }
}

// Offline Storage Manager using IndexedDB
class OfflineStorageManager {
  constructor() {
    this.dbName = 'PWAOfflineStorage';
    this.dbVersion = 1;
    this.stores = {
      userData: 'userData',
      appState: 'appState',
      cache: 'cache',
      syncQueue: 'syncQueue',
    };
    this.db = null;
  }

  // Initialize IndexedDB
  async initialize() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.dbVersion);

      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        this.db = request.result;
        resolve(this.db);
      };

      request.onupgradeneeded = (event) => {
        const db = event.target.result;

        // Create object stores
        Object.values(this.stores).forEach((storeName) => {
          if (!db.objectStoreNames.contains(storeName)) {
            const store = db.createObjectStore(storeName, {
              keyPath: 'id',
              autoIncrement: true,
            });

            store.createIndex('timestamp', 'timestamp', { unique: false });
            store.createIndex('key', 'key', { unique: false });
          }
        });
      };
    });
  }

  // Store data
  async store(storeName, key, data) {
    if (!this.db) {
      await this.initialize();
    }

    const record = {
      key,
      data,
      timestamp: Date.now(),
    };

    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([storeName], 'readwrite');
      const store = transaction.objectStore(storeName);

      const request = store.add(record);

      request.onerror = () => reject(request.error);
      request.onsuccess = () => resolve(request.result);
    });
  }

  // Retrieve data
  async retrieve(storeName, key) {
    if (!this.db) {
      await this.initialize();
    }

    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([storeName], 'readonly');
      const store = transaction.objectStore(storeName);
      const index = store.index('key');

      const request = index.get(key);

      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        const result = request.result;
        resolve(result ? result.data : null);
      };
    });
  }

  // Update data
  async update(storeName, key, data) {
    if (!this.db) {
      await this.initialize();
    }

    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([storeName], 'readwrite');
      const store = transaction.objectStore(storeName);
      const index = store.index('key');

      const getRequest = index.get(key);

      getRequest.onerror = () => reject(getRequest.error);
      getRequest.onsuccess = () => {
        const existing = getRequest.result;

        if (existing) {
          existing.data = data;
          existing.timestamp = Date.now();

          const putRequest = store.put(existing);
          putRequest.onerror = () => reject(putRequest.error);
          putRequest.onsuccess = () => resolve(putRequest.result);
        } else {
          resolve(null);
        }
      };
    });
  }

  // Delete data
  async delete(storeName, key) {
    if (!this.db) {
      await this.initialize();
    }

    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([storeName], 'readwrite');
      const store = transaction.objectStore(storeName);
      const index = store.index('key');

      const getRequest = index.get(key);

      getRequest.onerror = () => reject(getRequest.error);
      getRequest.onsuccess = () => {
        const existing = getRequest.result;

        if (existing) {
          const deleteRequest = store.delete(existing.id);
          deleteRequest.onerror = () => reject(deleteRequest.error);
          deleteRequest.onsuccess = () => resolve(true);
        } else {
          resolve(false);
        }
      };
    });
  }

  // Get all data from store
  async getAll(storeName) {
    if (!this.db) {
      await this.initialize();
    }

    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([storeName], 'readonly');
      const store = transaction.objectStore(storeName);

      const request = store.getAll();

      request.onerror = () => reject(request.error);
      request.onsuccess = () => resolve(request.result);
    });
  }

  // Clear store
  async clear(storeName) {
    if (!this.db) {
      await this.initialize();
    }

    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([storeName], 'readwrite');
      const store = transaction.objectStore(storeName);

      const request = store.clear();

      request.onerror = () => reject(request.error);
      request.onsuccess = () => resolve();
    });
  }
}

// Background Sync Manager
class BackgroundSyncManager {
  constructor(offlineStorage) {
    this.offlineStorage = offlineStorage;
    this.storeName = 'syncQueue';
  }

  // Add request to sync queue
  async addToSyncQueue(url, method = 'GET', body = null, headers = {}) {
    const request = {
      url,
      method,
      body: body ? JSON.stringify(body) : null,
      headers: {
        'Content-Type': 'application/json',
        ...headers,
      },
      timestamp: Date.now(),
      retries: 0,
      maxRetries: 3,
    };

    await this.offlineStorage.store(
      this.storeName,
      `${Date.now()}_${Math.random()}`,
      request
    );

    // Register background sync
    this.registerBackgroundSync();

    return request;
  }

  // Register background sync
  async registerBackgroundSync() {
    if (
      'serviceWorker' in navigator &&
      'sync' in window.ServiceWorkerRegistration.prototype
    ) {
      try {
        const registration = await navigator.serviceWorker.ready;
        await registration.sync.register('background-sync');
        console.log('Background sync registered');
      } catch (error) {
        console.error('Background sync registration failed:', error);
      }
    } else {
      console.warn(
        'Background sync not supported, falling back to immediate sync'
      );
      this.syncPendingData();
    }
  }

  // Sync pending data immediately
  async syncPendingData() {
    const pendingRequests = await this.offlineStorage.getAll(this.storeName);

    for (const requestRecord of pendingRequests) {
      const request = requestRecord.data;

      try {
        const response = await fetch(request.url, {
          method: request.method,
          body: request.body,
          headers: request.headers,
        });

        if (response.ok) {
          await this.offlineStorage.delete(this.storeName, requestRecord.key);
          console.log('Sync successful for:', request.url);
        } else {
          // Increment retries
          request.retries++;
          if (request.retries >= request.maxRetries) {
            await this.offlineStorage.delete(this.storeName, requestRecord.key);
          } else {
            await this.offlineStorage.update(
              this.storeName,
              requestRecord.key,
              request
            );
          }
        }
      } catch (error) {
        console.error('Sync failed for:', request.url, error);
        // Increment retries
        request.retries++;
        if (request.retries >= request.maxRetries) {
          await this.offlineStorage.delete(this.storeName, requestRecord.key);
        } else {
          await this.offlineStorage.update(
            this.storeName,
            requestRecord.key,
            request
          );
        }
      }
    }
  }

  // Get sync queue status
  async getQueueStatus() {
    const pendingRequests = await this.offlineStorage.getAll(this.storeName);

    return {
      totalRequests: pendingRequests.length,
      byMethod: pendingRequests.reduce((acc, req) => {
        const method = req.data.method;
        acc[method] = (acc[method] || 0) + 1;
        return acc;
      }, {}),
      oldestRequest:
        pendingRequests.length > 0
          ? Math.min(...pendingRequests.map((req) => req.data.timestamp))
          : null,
    };
  }
}

// Usage example
async function initializePWAFeatures() {
  // Initialize storage
  const offlineStorage = new OfflineStorageManager();
  await offlineStorage.initialize();

  // Initialize managers
  const notificationManager = new NotificationManager();
  const syncManager = new BackgroundSyncManager(offlineStorage);

  // Example: Store user data for offline access
  await offlineStorage.store('userData', 'profile', {
    name: 'John Doe',
    email: 'john@example.com',
    preferences: {
      theme: 'dark',
      notifications: true,
    },
  });

  // Example: Store app state
  await offlineStorage.store('appState', 'currentView', {
    route: '/dashboard',
    filters: { status: 'active' },
    pagination: { page: 1, limit: 20 },
  });

  console.log('PWA features initialized');

  return { offlineStorage, notificationManager, syncManager };
}

initializePWAFeatures()
  .then((result) => {
    console.log('PWA features ready');
  })
  .catch((error) => {
    console.error('PWA features initialization failed:', error);
  });

Service Worker Implementation

Complete Service Worker Template

// Service Worker Implementation - This goes in sw.js file
const SERVICE_WORKER_VERSION = '1.0.0';
const CACHE_NAME = `pwa-cache-v${SERVICE_WORKER_VERSION}`;
const STATIC_CACHE = `static-cache-v${SERVICE_WORKER_VERSION}`;
const DYNAMIC_CACHE = `dynamic-cache-v${SERVICE_WORKER_VERSION}`;
const API_CACHE = `api-cache-v${SERVICE_WORKER_VERSION}`;

// Files to cache on install
const STATIC_FILES = [
  '/',
  '/index.html',
  '/manifest.json',
  '/styles/main.css',
  '/scripts/main.js',
  '/images/icon-192.png',
  '/images/icon-512.png',
  '/offline.html',
];

// Install event
self.addEventListener('install', (event) => {
  console.log('Service Worker: Installing version', SERVICE_WORKER_VERSION);

  event.waitUntil(
    caches
      .open(STATIC_CACHE)
      .then((cache) => {
        console.log('Service Worker: Caching static files');
        return cache.addAll(STATIC_FILES);
      })
      .then(() => {
        console.log('Service Worker: Static files cached');
        return self.skipWaiting();
      })
      .catch((error) => {
        console.error('Service Worker: Install failed', error);
      })
  );
});

// Activate event
self.addEventListener('activate', (event) => {
  console.log('Service Worker: Activating version', SERVICE_WORKER_VERSION);

  event.waitUntil(
    Promise.all([
      // Clean up old caches
      caches.keys().then((cacheNames) => {
        return Promise.all(
          cacheNames.map((cacheName) => {
            if (
              cacheName !== STATIC_CACHE &&
              cacheName !== DYNAMIC_CACHE &&
              cacheName !== API_CACHE
            ) {
              console.log('Service Worker: Deleting old cache:', cacheName);
              return caches.delete(cacheName);
            }
          })
        );
      }),
      // Claim all clients
      self.clients.claim(),
    ])
  );
});

// Fetch event with advanced routing
self.addEventListener('fetch', (event) => {
  const { request } = event;
  const url = new URL(request.url);

  // Skip non-GET requests
  if (request.method !== 'GET') {
    return;
  }

  // Handle different types of requests
  if (url.pathname.startsWith('/api/')) {
    // API requests - Network first with cache fallback
    event.respondWith(handleAPIRequest(request));
  } else if (
    url.pathname.match(/\.(css|js|png|jpg|jpeg|gif|svg|woff|woff2|ico)$/)
  ) {
    // Static assets - Cache first
    event.respondWith(handleStaticAssets(request));
  } else if (url.pathname === '/' || url.pathname.endsWith('.html')) {
    // HTML pages - Stale while revalidate
    event.respondWith(handleHTMLPages(request));
  } else {
    // Everything else - Network first
    event.respondWith(handleOtherRequests(request));
  }
});

// API request handler
async function handleAPIRequest(request) {
  const cache = await caches.open(API_CACHE);

  try {
    // Try network first
    const networkResponse = await fetch(request);

    if (networkResponse.ok) {
      // Cache successful responses with timestamp
      const responseClone = networkResponse.clone();

      const response = new Response(responseClone.body, {
        status: responseClone.status,
        statusText: responseClone.statusText,
        headers: {
          ...Object.fromEntries(responseClone.headers.entries()),
          'sw-cache-timestamp': Date.now().toString(),
        },
      });

      cache.put(request, response);
    }

    return networkResponse;
  } catch (error) {
    console.warn('Network request failed, trying cache:', error);

    const cachedResponse = await cache.match(request);

    if (cachedResponse) {
      // Check if cache is expired (5 minutes for API)
      const cacheTimestamp = cachedResponse.headers.get('sw-cache-timestamp');
      const isExpired =
        cacheTimestamp && Date.now() - parseInt(cacheTimestamp) > 5 * 60 * 1000;

      if (!isExpired) {
        return cachedResponse;
      }
    }

    // Return offline response
    return new Response(
      JSON.stringify({ error: 'Offline', message: 'Unable to fetch data' }),
      {
        status: 503,
        statusText: 'Service Unavailable',
        headers: { 'Content-Type': 'application/json' },
      }
    );
  }
}

// Static assets handler
async function handleStaticAssets(request) {
  const cache = await caches.open(STATIC_CACHE);
  const cachedResponse = await cache.match(request);

  if (cachedResponse) {
    return cachedResponse;
  }

  try {
    const networkResponse = await fetch(request);

    if (networkResponse.ok) {
      cache.put(request, networkResponse.clone());
    }

    return networkResponse;
  } catch (error) {
    console.error('Failed to fetch static asset:', error);

    // Return a fallback for failed requests
    if (request.url.match(/\.(png|jpg|jpeg|gif|svg)$/)) {
      return new Response(
        '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"><rect width="200" height="200" fill="#f0f0f0"/><text x="50%" y="50%" text-anchor="middle" fill="#666">Image not available</text></svg>',
        { headers: { 'Content-Type': 'image/svg+xml' } }
      );
    }

    throw error;
  }
}

// HTML pages handler
async function handleHTMLPages(request) {
  const cache = await caches.open(DYNAMIC_CACHE);
  const cachedResponse = await cache.match(request);

  // Start network request in background
  const networkPromise = fetch(request)
    .then((response) => {
      if (response.ok) {
        cache.put(request, response.clone());
      }
      return response;
    })
    .catch((error) => {
      console.warn('Network request failed:', error);
      return null;
    });

  // Return cached response immediately if available
  if (cachedResponse) {
    return cachedResponse;
  }

  // Wait for network response
  const networkResponse = await networkPromise;

  if (networkResponse) {
    return networkResponse;
  }

  // Return offline page
  const offlineResponse = await cache.match('/offline.html');
  return offlineResponse || new Response('Offline', { status: 503 });
}

// Other requests handler
async function handleOtherRequests(request) {
  try {
    return await fetch(request);
  } catch (error) {
    console.error('Request failed:', error);

    const cache = await caches.open(DYNAMIC_CACHE);
    const cachedResponse = await cache.match(request);

    return cachedResponse || new Response('Offline', { status: 503 });
  }
}

// Background sync
self.addEventListener('sync', (event) => {
  console.log('Service Worker: Background sync triggered for tag:', event.tag);

  if (event.tag === 'background-sync') {
    event.waitUntil(doBackgroundSync());
  }
});

async function doBackgroundSync() {
  try {
    // Get pending requests from IndexedDB
    const db = await openIndexedDB();
    const pendingRequests = await getPendingRequests(db);

    let syncedCount = 0;

    for (const requestData of pendingRequests) {
      try {
        const response = await fetch(requestData.url, {
          method: requestData.method,
          body: requestData.body,
          headers: requestData.headers,
        });

        if (response.ok) {
          await removePendingRequest(db, requestData.id);
          syncedCount++;
        }
      } catch (error) {
        console.error('Sync failed for request:', requestData.url, error);
      }
    }

    // Notify all clients
    const clients = await self.clients.matchAll();
    clients.forEach((client) => {
      client.postMessage({
        type: 'SYNC_COMPLETE',
        payload: { synced: syncedCount, total: pendingRequests.length },
      });
    });
  } catch (error) {
    console.error('Background sync failed:', error);
  }
}

// Push notifications
self.addEventListener('push', (event) => {
  console.log('Service Worker: Push received');

  let notificationData = {
    title: 'PWA Notification',
    body: 'You have a new notification',
    icon: '/images/icon-192.png',
    badge: '/images/badge.png',
    vibrate: [100, 50, 100],
    data: {
      dateOfArrival: Date.now(),
      primaryKey: Math.random(),
    },
    actions: [
      {
        action: 'explore',
        title: 'View',
        icon: '/images/view-icon.png',
      },
      {
        action: 'close',
        title: 'Close',
        icon: '/images/close-icon.png',
      },
    ],
    requireInteraction: true,
  };

  if (event.data) {
    try {
      const payload = event.data.json();
      notificationData = { ...notificationData, ...payload };
    } catch (error) {
      console.error('Failed to parse push data:', error);
    }
  }

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

// Notification click
self.addEventListener('notificationclick', (event) => {
  console.log('Service Worker: Notification clicked');

  event.notification.close();

  if (event.action === 'explore') {
    event.waitUntil(clients.openWindow(event.notification.data?.url || '/'));
  } else if (event.action === 'close') {
    // Just close the notification
  } else {
    // Default action - open the app
    event.waitUntil(clients.openWindow('/'));
  }
});

// Message handling
self.addEventListener('message', async (event) => {
  console.log('Service Worker: Message received:', event.data);

  const { action, data } = event.data;

  switch (action) {
    case 'skipWaiting':
      self.skipWaiting();
      break;

    case 'getCacheSize':
      const cacheSize = await getCacheSize();
      event.ports[0].postMessage({ cacheSize });
      break;

    case 'clearCache':
      await clearAllCaches();
      event.ports[0].postMessage({ success: true });
      break;

    case 'updateCache':
      await updateStaticCache();
      event.ports[0].postMessage({ success: true });
      break;
  }
});

// Utility functions for IndexedDB operations
async function openIndexedDB() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open('PWADatabase', 1);

    request.onerror = () => reject(request.error);
    request.onsuccess = () => resolve(request.result);

    request.onupgradeneeded = (event) => {
      const db = event.target.result;
      if (!db.objectStoreNames.contains('pendingSync')) {
        const store = db.createObjectStore('pendingSync', {
          keyPath: 'id',
          autoIncrement: true,
        });
        store.createIndex('timestamp', 'timestamp', { unique: false });
      }
    };
  });
}

async function getPendingRequests(db) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['pendingSync'], 'readonly');
    const store = transaction.objectStore('pendingSync');
    const request = store.getAll();

    request.onerror = () => reject(request.error);
    request.onsuccess = () => resolve(request.result);
  });
}

async function removePendingRequest(db, id) {
  return new Promise((resolve, reject) => {
    const transaction = db.transaction(['pendingSync'], 'readwrite');
    const store = transaction.objectStore('pendingSync');
    const request = store.delete(id);

    request.onerror = () => reject(request.error);
    request.onsuccess = () => resolve();
  });
}

async function getCacheSize() {
  const cacheNames = await caches.keys();
  let totalSize = 0;

  for (const cacheName of cacheNames) {
    const cache = await caches.open(cacheName);
    const requests = await cache.keys();

    for (const request of requests) {
      const response = await cache.match(request);
      if (response) {
        const blob = await response.blob();
        totalSize += blob.size;
      }
    }
  }

  return totalSize;
}

async function clearAllCaches() {
  const cacheNames = await caches.keys();
  return Promise.all(cacheNames.map((cacheName) => caches.delete(cacheName)));
}

async function updateStaticCache() {
  const cache = await caches.open(STATIC_CACHE);
  return cache.addAll(STATIC_FILES);
}

console.log('Service Worker: Script loaded, version', SERVICE_WORKER_VERSION);

Conclusion

Progressive Web Apps represent the future of web applications, combining the best of web and native app experiences. Through service workers, advanced caching strategies, push notifications, and offline functionality, PWAs deliver app-like experiences that users expect from modern applications. The key to successful PWA development is implementing robust caching strategies, providing seamless offline experiences, and optimizing for performance across different network conditions.

When building PWAs with JavaScript, focus on progressive enhancement, ensuring your app works well even when advanced features aren't supported. Implement proper analytics to understand user engagement, provide clear installation prompts, and maintain your service workers with proper update mechanisms. PWAs can significantly improve user engagement, reduce bounce rates, and provide native app-like experiences without the friction of app store installations.