Building Progressive Web Apps with Service Workers
1/18/2024
14 min read
Tridip Dutta
Web Development

Building Progressive Web Apps with Service Workers

Complete guide to creating PWAs that work offline, send push notifications, and provide native app-like experiences on the web.

PWA
Service Workers
Offline
Web Development

Building Progressive Web Apps with Service Workers

Progressive Web Apps (PWAs) bridge the gap between web and native applications, offering app-like experiences directly in the browser. At the heart of PWAs are Service Workers—powerful scripts that enable offline functionality, background sync, and push notifications.

What are Progressive Web Apps?

PWAs are web applications that use modern web capabilities to provide users with an app-like experience. They combine the best of web and mobile apps.

Key PWA Features:

  • Offline functionality through Service Workers
  • App-like interface with responsive design
  • Push notifications for user engagement
  • Installable on device home screens
  • Secure (HTTPS required)
  • Progressive enhancement for all browsers

Understanding Service Workers

Service Workers are scripts that run in the background, separate from your web page, enabling features that don't need a web page or user interaction.

Service Worker Capabilities:

  • Intercept network requests and serve cached responses
  • Background sync for offline actions
  • Push notifications from servers
  • Cache management for offline content

Basic Service Worker Registration:

// main.js
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js')
      .then(registration => {
        console.log('SW registered: ', registration);
      })
      .catch(registrationError => {
        console.log('SW registration failed: ', registrationError);
      });
  });
}

Implementing Offline Functionality

Cache Strategies

Different caching strategies serve different use cases:

1. Cache First Strategy

// sw.js
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        // Return cached version or fetch from network
        return response || fetch(event.request);
      })
  );
});

2. Network First Strategy

self.addEventListener('fetch', event => {
  event.respondWith(
    fetch(event.request)
      .then(response => {
        // Clone and cache the response
        const responseClone = response.clone();
        caches.open('v1').then(cache => {
          cache.put(event.request, responseClone);
        });
        return response;
      })
      .catch(() => {
        // Fallback to cache if network fails
        return caches.match(event.request);
      })
  );
});

3. Stale While Revalidate

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.open('v1').then(cache => {
      return cache.match(event.request).then(cachedResponse => {
        const fetchPromise = fetch(event.request).then(networkResponse => {
          cache.put(event.request, networkResponse.clone());
          return networkResponse;
        });
        
        // Return cached response immediately, update cache in background
        return cachedResponse || fetchPromise;
      });
    })
  );
});

Complete Service Worker Example

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

// Install event - cache resources
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => {
        return cache.addAll(urlsToCache);
      })
  );
});

// Activate event - clean up old caches
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (cacheName !== CACHE_NAME) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

// Fetch event - serve cached content when offline
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        // Return cached version or fetch from network
        if (response) {
          return response;
        }
        
        return fetch(event.request).then(response => {
          // Don't cache non-successful responses
          if (!response || response.status !== 200 || response.type !== 'basic') {
            return response;
          }
          
          // Clone the response
          const responseToCache = response.clone();
          
          caches.open(CACHE_NAME)
            .then(cache => {
              cache.put(event.request, responseToCache);
            });
          
          return response;
        });
      })
      .catch(() => {
        // Show offline page for navigation requests
        if (event.request.destination === 'document') {
          return caches.match('/offline.html');
        }
      })
  );
});

Web App Manifest

The Web App Manifest provides metadata about your application:

{
  "name": "My Progressive Web App",
  "short_name": "MyPWA",
  "description": "A sample progressive web application",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#000000",
  "icons": [
    {
      "src": "/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

Link the manifest in your HTML:

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

Push Notifications

Setting Up Push Notifications

// Request notification permission
function requestNotificationPermission() {
  return Notification.requestPermission().then(permission => {
    if (permission === 'granted') {
      console.log('Notification permission granted');
      return subscribeUserToPush();
    }
  });
}

// Subscribe to push notifications
function subscribeUserToPush() {
  return navigator.serviceWorker.register('/sw.js')
    .then(registration => {
      const subscribeOptions = {
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array(publicVapidKey)
      };
      
      return registration.pushManager.subscribe(subscribeOptions);
    })
    .then(pushSubscription => {
      console.log('User is subscribed:', pushSubscription);
      return sendSubscriptionToBackEnd(pushSubscription);
    });
}

// Handle push events in service worker
self.addEventListener('push', event => {
  const options = {
    body: event.data ? event.data.text() : 'No payload',
    icon: '/icons/icon-192x192.png',
    badge: '/icons/badge-72x72.png',
    actions: [
      {
        action: 'explore',
        title: 'Explore',
        icon: '/icons/checkmark.png'
      },
      {
        action: 'close',
        title: 'Close',
        icon: '/icons/xmark.png'
      }
    ]
  };
  
  event.waitUntil(
    self.registration.showNotification('PWA Notification', options)
  );
});

Background Sync

Background Sync allows you to defer actions until the user has stable connectivity:

// Register background sync
navigator.serviceWorker.ready.then(registration => {
  return registration.sync.register('background-sync');
});

// Handle background sync in service worker
self.addEventListener('sync', event => {
  if (event.tag === 'background-sync') {
    event.waitUntil(doBackgroundSync());
  }
});

function doBackgroundSync() {
  return fetch('/api/sync-data', {
    method: 'POST',
    body: JSON.stringify(getStoredData())
  });
}

PWA Best Practices

1. App Shell Architecture

Create a minimal HTML, CSS, and JavaScript that powers the user interface:

const appShellFiles = [
  '/',
  '/app-shell.html',
  '/styles/app-shell.css',
  '/scripts/app-shell.js'
];

// Cache app shell during install
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open('app-shell-v1')
      .then(cache => cache.addAll(appShellFiles))
  );
});

2. Performance Optimization

  • Lazy load non-critical resources
  • Preload critical resources
  • Optimize images for different screen sizes
  • Minimize JavaScript bundles

3. Responsive Design

Ensure your PWA works across all device sizes:

/* Responsive design for PWA */
@media (max-width: 768px) {
  .container {
    padding: 1rem;
  }
}

@media (display-mode: standalone) {
  /* Styles when app is installed */
  .install-prompt {
    display: none;
  }
}

Testing PWAs

Tools for PWA Testing:

  1. Lighthouse - Automated PWA auditing
  2. Chrome DevTools - Application panel for debugging
  3. PWA Builder - Microsoft's PWA testing tool
  4. Workbox - Google's PWA library

Lighthouse PWA Checklist:

  • ✅ Served over HTTPS
  • ✅ Responsive design
  • ✅ Offline functionality
  • ✅ Web app manifest
  • ✅ Service worker registered
  • ✅ Fast load times

Deployment Considerations

HTTPS Requirement

PWAs require HTTPS for security. Service Workers only work over secure connections.

App Store Distribution

PWAs can be distributed through:

  • Google Play Store (via Trusted Web Activities)
  • Microsoft Store (via PWA Builder)
  • Direct installation from browsers

Common Pitfalls and Solutions

1. Cache Management

// Avoid: Caching everything
// Do: Selective caching based on request type
self.addEventListener('fetch', event => {
  // Only cache GET requests
  if (event.request.method !== 'GET') return;
  
  // Skip caching for API calls that should always be fresh
  if (event.request.url.includes('/api/live-data')) return;
  
  // Cache static assets
  if (event.request.destination === 'image' || 
      event.request.destination === 'script' || 
      event.request.destination === 'style') {
    event.respondWith(cacheFirst(event.request));
  }
});

2. Service Worker Updates

// Handle service worker updates gracefully
self.addEventListener('message', event => {
  if (event.data && event.data.type === 'SKIP_WAITING') {
    self.skipWaiting();
  }
});

// In your main app
navigator.serviceWorker.addEventListener('controllerchange', () => {
  // Reload page when new service worker takes control
  window.location.reload();
});

Conclusion

Progressive Web Apps with Service Workers offer a powerful way to create app-like experiences on the web. They provide offline functionality, push notifications, and improved performance while maintaining the reach and accessibility of web applications.

Start with basic offline functionality, then gradually add features like push notifications and background sync. Remember to test thoroughly across different devices and network conditions.

Resources


PWAs represent the future of web applications, combining the best of web and native app experiences. Start building your PWA today and provide users with fast, reliable, and engaging experiences.

TD

About Tridip Dutta

Creative Developer passionate about creating innovative digital experiences and exploring AI. I love sharing knowledge to help developers build better apps.