`n

PWA Implementation - Complete Guide

Published: September 25, 2024 | Reading time: 26 minutes

PWA Implementation Overview

Progressive Web Apps provide native app-like experiences in the browser:

PWA Benefits
# PWA Benefits
- Offline functionality
- App-like experience
- Push notifications
- Installable
- Fast loading
- Responsive design
- Secure (HTTPS)

Service Worker Implementation

Service Worker Setup and Caching

Service Worker Implementation
# Service Worker Implementation

# 1. Basic Service Worker Registration
// app.js
if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () => {
    try {
      const registration = await navigator.serviceWorker.register('/sw.js');
      console.log('Service Worker registered successfully:', registration);
    } catch (error) {
      console.log('Service Worker registration failed:', error);
    }
  });
}

// sw.js
const CACHE_NAME = 'my-app-cache-v1';
const urlsToCache = [
  '/',
  '/static/css/main.css',
  '/static/js/main.js',
  '/images/logo.png',
  '/manifest.json'
];

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

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

# 2. Advanced Caching Strategies
// sw.js - Advanced caching
const CACHE_NAME = 'my-app-cache-v2';
const STATIC_CACHE = 'static-cache-v1';
const DYNAMIC_CACHE = 'dynamic-cache-v1';

// Install event
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(STATIC_CACHE)
      .then((cache) => {
        return cache.addAll([
          '/',
          '/static/css/main.css',
          '/static/js/main.js',
          '/images/logo.png',
          '/manifest.json'
        ]);
      })
  );
  self.skipWaiting();
});

// Activate event
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames.map((cacheName) => {
          if (cacheName !== STATIC_CACHE && cacheName !== DYNAMIC_CACHE) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
  self.clients.claim();
});

// Fetch event with different strategies
self.addEventListener('fetch', (event) => {
  const { request } = event;
  const url = new URL(request.url);
  
  // Cache first strategy for static assets
  if (request.destination === 'image' || 
      request.destination === 'style' || 
      request.destination === 'script') {
    event.respondWith(cacheFirst(request));
  }
  // Network first strategy for API calls
  else if (url.pathname.startsWith('/api/')) {
    event.respondWith(networkFirst(request));
  }
  // Stale while revalidate for HTML pages
  else {
    event.respondWith(staleWhileRevalidate(request));
  }
});

// Cache first strategy
async function cacheFirst(request) {
  const cachedResponse = await caches.match(request);
  if (cachedResponse) {
    return cachedResponse;
  }
  
  const networkResponse = await fetch(request);
  if (networkResponse.ok) {
    const cache = await caches.open(STATIC_CACHE);
    cache.put(request, networkResponse.clone());
  }
  
  return networkResponse;
}

// Network first strategy
async function networkFirst(request) {
  try {
    const networkResponse = await fetch(request);
    if (networkResponse.ok) {
      const cache = await caches.open(DYNAMIC_CACHE);
      cache.put(request, networkResponse.clone());
    }
    return networkResponse;
  } catch (error) {
    const cachedResponse = await caches.match(request);
    if (cachedResponse) {
      return cachedResponse;
    }
    return new Response('Offline', { status: 503 });
  }
}

// Stale while revalidate strategy
async function staleWhileRevalidate(request) {
  const cache = await caches.open(DYNAMIC_CACHE);
  const cachedResponse = await cache.match(request);
  
  const fetchPromise = fetch(request).then((networkResponse) => {
    if (networkResponse.ok) {
      cache.put(request, networkResponse.clone());
    }
    return networkResponse;
  });
  
  return cachedResponse || fetchPromise;
}

# 3. Background Sync
// sw.js - Background sync
self.addEventListener('sync', (event) => {
  if (event.tag === 'background-sync') {
    event.waitUntil(doBackgroundSync());
  }
});

async function doBackgroundSync() {
  try {
    const requests = await getStoredRequests();
    for (const request of requests) {
      try {
        await fetch(request.url, request.options);
        await removeStoredRequest(request.id);
      } catch (error) {
        console.log('Background sync failed:', error);
      }
    }
  } catch (error) {
    console.log('Background sync error:', error);
  }
}

// Store requests when offline
async function storeRequest(url, options) {
  const request = {
    id: Date.now(),
    url,
    options,
    timestamp: Date.now()
  };
  
  const requests = await getStoredRequests();
  requests.push(request);
  
  localStorage.setItem('offline-requests', JSON.stringify(requests));
  
  // Register background sync
  if ('serviceWorker' in navigator && 'sync' in window.ServiceWorkerRegistration.prototype) {
    const registration = await navigator.serviceWorker.ready;
    registration.sync.register('background-sync');
  }
}

async function getStoredRequests() {
  const stored = localStorage.getItem('offline-requests');
  return stored ? JSON.parse(stored) : [];
}

async function removeStoredRequest(id) {
  const requests = await getStoredRequests();
  const filtered = requests.filter(req => req.id !== id);
  localStorage.setItem('offline-requests', JSON.stringify(filtered));
}

# 4. Push Notifications
// sw.js - Push notifications
self.addEventListener('push', (event) => {
  const options = {
    body: event.data ? event.data.text() : 'You have a new notification',
    icon: '/images/icon-192x192.png',
    badge: '/images/badge-72x72.png',
    vibrate: [100, 50, 100],
    data: {
      dateOfArrival: Date.now(),
      primaryKey: 1
    },
    actions: [
      {
        action: 'explore',
        title: 'View',
        icon: '/images/checkmark.png'
      },
      {
        action: 'close',
        title: 'Close',
        icon: '/images/xmark.png'
      }
    ]
  };
  
  event.waitUntil(
    self.registration.showNotification('My App', options)
  );
});

// Handle notification clicks
self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  
  if (event.action === 'explore') {
    event.waitUntil(
      clients.openWindow('/')
    );
  } else if (event.action === 'close') {
    // Just close the notification
  } else {
    // Default action
    event.waitUntil(
      clients.openWindow('/')
    );
  }
});

# 5. Web App Manifest
// manifest.json
{
  "name": "My Progressive Web App",
  "short_name": "MyPWA",
  "description": "A sample Progressive Web App",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#000000",
  "orientation": "portrait-primary",
  "scope": "/",
  "lang": "en",
  "dir": "ltr",
  "icons": [
    {
      "src": "/images/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/images/icon-96x96.png",
      "sizes": "96x96",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/images/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/images/icon-144x144.png",
      "sizes": "144x144",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/images/icon-152x152.png",
      "sizes": "152x152",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/images/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/images/icon-384x384.png",
      "sizes": "384x384",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/images/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ],
  "categories": ["productivity", "utilities"],
  "screenshots": [
    {
      "src": "/images/screenshot1.png",
      "sizes": "1280x720",
      "type": "image/png",
      "form_factor": "wide"
    },
    {
      "src": "/images/screenshot2.png",
      "sizes": "750x1334",
      "type": "image/png",
      "form_factor": "narrow"
    }
  ],
  "shortcuts": [
    {
      "name": "New Item",
      "short_name": "New",
      "description": "Create a new item",
      "url": "/new",
      "icons": [
        {
          "src": "/images/shortcut-new.png",
          "sizes": "96x96"
        }
      ]
    }
  ]
}

# 6. PWA Installation
// app.js - PWA installation
let deferredPrompt;

window.addEventListener('beforeinstallprompt', (event) => {
  event.preventDefault();
  deferredPrompt = event;
  
  // Show install button
  const installButton = document.getElementById('install-button');
  installButton.style.display = 'block';
  installButton.addEventListener('click', installApp);
});

async function installApp() {
  if (deferredPrompt) {
    deferredPrompt.prompt();
    const { outcome } = await deferredPrompt.userChoice;
    
    if (outcome === 'accepted') {
      console.log('User accepted the install prompt');
    } else {
      console.log('User dismissed the install prompt');
    }
    
    deferredPrompt = null;
    
    // Hide install button
    const installButton = document.getElementById('install-button');
    installButton.style.display = 'none';
  }
}

// Check if app is already installed
window.addEventListener('appinstalled', (event) => {
  console.log('PWA was installed');
});

// Detect if running as PWA
function isPWA() {
  return window.matchMedia('(display-mode: standalone)').matches ||
         window.navigator.standalone === true;
}

# 7. Offline Page
// offline.html



  
  
  Offline - My App
  

  
📱

You're Offline

It looks like you're not connected to the internet. Some features may not be available.

# 8. PWA Update Management // app.js - PWA update management let registration; if ('serviceWorker' in navigator) { window.addEventListener('load', async () => { registration = await navigator.serviceWorker.register('/sw.js'); registration.addEventListener('updatefound', () => { const newWorker = registration.installing; newWorker.addEventListener('statechange', () => { if (newWorker.state === 'installed') { if (navigator.serviceWorker.controller) { // New content is available showUpdateNotification(); } else { // Content is cached for the first time console.log('Content is cached for offline use'); } } }); }); }); } function showUpdateNotification() { const updateNotification = document.createElement('div'); updateNotification.innerHTML = `

New version available!

`; document.body.appendChild(updateNotification); } function updateApp() { if (registration && registration.waiting) { registration.waiting.postMessage({ type: 'SKIP_WAITING' }); window.location.reload(); } } # 9. PWA Testing // pwa-test.js class PWATester { constructor() { this.results = {}; } async runTests() { console.log('Running PWA tests...'); this.results.serviceWorker = await this.testServiceWorker(); this.results.manifest = await this.testManifest(); this.results.https = await this.testHTTPS(); this.results.responsive = await this.testResponsive(); this.results.offline = await this.testOffline(); this.generateReport(); } async testServiceWorker() { if (!('serviceWorker' in navigator)) { return { status: 'fail', message: 'Service Worker not supported' }; } try { const registration = await navigator.serviceWorker.getRegistration(); if (registration) { return { status: 'pass', message: 'Service Worker registered' }; } else { return { status: 'fail', message: 'Service Worker not registered' }; } } catch (error) { return { status: 'fail', message: error.message }; } } async testManifest() { try { const response = await fetch('/manifest.json'); if (response.ok) { const manifest = await response.json(); const requiredFields = ['name', 'short_name', 'start_url', 'display', 'icons']; const missingFields = requiredFields.filter(field => !manifest[field]); if (missingFields.length === 0) { return { status: 'pass', message: 'Manifest is valid' }; } else { return { status: 'fail', message: `Missing fields: ${missingFields.join(', ')}` }; } } else { return { status: 'fail', message: 'Manifest not found' }; } } catch (error) { return { status: 'fail', message: error.message }; } } async testHTTPS() { if (location.protocol === 'https:') { return { status: 'pass', message: 'HTTPS enabled' }; } else { return { status: 'fail', message: 'HTTPS required for PWA' }; } } async testResponsive() { const viewport = document.querySelector('meta[name="viewport"]'); if (viewport) { return { status: 'pass', message: 'Viewport meta tag present' }; } else { return { status: 'fail', message: 'Viewport meta tag missing' }; } } async testOffline() { try { const response = await fetch('/offline.html'); if (response.ok) { return { status: 'pass', message: 'Offline page available' }; } else { return { status: 'fail', message: 'Offline page not found' }; } } catch (error) { return { status: 'fail', message: error.message }; } } generateReport() { console.log('PWA Test Results:'); Object.entries(this.results).forEach(([test, result]) => { const status = result.status === 'pass' ? '✓' : '✗'; console.log(`${status} ${test}: ${result.message}`); }); } } // Run tests const tester = new PWATester(); tester.runTests(); # 10. PWA Performance Monitoring // pwa-performance.js class PWAPerformanceMonitor { constructor() { this.metrics = {}; } startMonitoring() { this.monitorServiceWorker(); this.monitorCacheUsage(); this.monitorOfflineUsage(); } monitorServiceWorker() { if ('serviceWorker' in navigator) { navigator.serviceWorker.addEventListener('message', (event) => { if (event.data.type === 'PERFORMANCE_METRIC') { this.metrics[event.data.name] = event.data.value; } }); } } monitorCacheUsage() { if ('storage' in navigator && 'estimate' in navigator.storage) { navigator.storage.estimate().then((estimate) => { this.metrics.cacheUsage = { used: estimate.usage, quota: estimate.quota, percentage: (estimate.usage / estimate.quota) * 100 }; }); } } monitorOfflineUsage() { window.addEventListener('online', () => { this.metrics.lastOnline = Date.now(); }); window.addEventListener('offline', () => { this.metrics.lastOffline = Date.now(); }); } getMetrics() { return this.metrics; } } const monitor = new PWAPerformanceMonitor(); monitor.startMonitoring();

PWA Best Practices

PWA Implementation Guidelines

PWA Components

  • Service Worker
  • Web App Manifest
  • HTTPS
  • Responsive Design
  • Offline Functionality
  • Push Notifications
  • App Shell

PWA Features

  • Installable
  • Offline-first
  • Fast loading
  • App-like experience
  • Background sync
  • Push notifications
  • Native integration

Summary

PWA implementation involves several key components:

  • Service Worker: Offline functionality, caching, and background sync
  • Web App Manifest: App metadata and installation behavior
  • HTTPS: Required for service workers and secure features
  • Responsive Design: Works across all devices and screen sizes

Need More Help?

Struggling with PWA implementation or need help creating a Progressive Web App? Our frontend experts can help you build engaging, offline-capable web applications.

Get PWA Help