DEV Community

Weather Clock Dash
Weather Clock Dash

Posted on

Progressive Web Apps: Service Workers, Offline Caching, and Background Sync

Progressive Web Apps (PWAs) bring native app-like capabilities to the web: offline support, push notifications, home screen installation, and fast loading on poor connections. The building blocks are a service worker, a web app manifest, and HTTPS.

What Makes a PWA

A PWA isn't a framework or tool — it's a set of features your web app gains:

  • Installable: users can add it to their home screen or taskbar
  • Offline capable: works without a network connection (or with a degraded fallback)
  • Fast: resources are cached; repeat visits load instantly
  • Reliable: doesn't break on flaky connections

You don't need all three features at once. Incrementally add the ones your users benefit from.

The Web App Manifest

The manifest tells the browser how to present your app when installed:

// manifest.json
{
  "name": "My App",
  "short_name": "MyApp",
  "description": "App that does useful things",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#2563eb",
  "icons": [
    { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
    { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Link it in your HTML:

<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#2563eb">
Enter fullscreen mode Exit fullscreen mode

display: "standalone" removes the browser UI (address bar, nav buttons) when launched from the home screen. Other options: minimal-ui, fullscreen, browser.

The maskable icon is important — Android applies a shape mask to home screen icons. Maskable icons are designed to fill the entire icon space without clipping.

Service Workers

A service worker is a JavaScript file that runs in the background, intercepting network requests and managing caches. It's the key to offline support.

// sw.js — register in your main JS:
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js');
}
Enter fullscreen mode Exit fullscreen mode

Cache-First Strategy (for static assets)

// sw.js
const CACHE_NAME = 'my-app-v1';
const STATIC_ASSETS = [
  '/',
  '/styles/main.css',
  '/scripts/app.js',
  '/icons/icon-192.png'
];

// Install: cache static assets
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => cache.addAll(STATIC_ASSETS))
  );
  self.skipWaiting();
});

// Activate: clean up old caches
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(keys =>
      Promise.all(
        keys
          .filter(key => key !== CACHE_NAME)
          .map(key => caches.delete(key))
      )
    )
  );
  self.clients.claim();
});

// Fetch: serve from cache, fall back to network
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(cached => {
      return cached || fetch(event.request);
    })
  );
});
Enter fullscreen mode Exit fullscreen mode

Network-First Strategy (for API data)

For dynamic content, try the network first and fall back to cache:

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  // Static assets: cache-first
  if (url.pathname.startsWith('/static/')) {
    event.respondWith(cacheFirst(event.request));
    return;
  }

  // API calls: network-first
  if (url.pathname.startsWith('/api/')) {
    event.respondWith(networkFirst(event.request));
    return;
  }
});

async function cacheFirst(request) {
  const cached = await caches.match(request);
  return cached || fetch(request);
}

async function networkFirst(request) {
  const cache = await caches.open('api-cache');
  try {
    const response = await fetch(request);
    cache.put(request, response.clone());
    return response;
  } catch {
    return cache.match(request);
  }
}
Enter fullscreen mode Exit fullscreen mode

Stale-While-Revalidate (best of both)

Serve cached content immediately, update cache in the background:

async function staleWhileRevalidate(request) {
  const cache = await caches.open('swre-cache');
  const cached = await cache.match(request);

  const fetchPromise = fetch(request).then(response => {
    cache.put(request, response.clone());
    return response;
  });

  return cached || fetchPromise;
}
Enter fullscreen mode Exit fullscreen mode

Offline Fallback Page

When a navigation request fails (user visits a page they haven't cached), show a fallback:

// Cache the offline page during install
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache =>
      cache.addAll(['/offline.html', ...STATIC_ASSETS])
    )
  );
});

// Serve offline page for failed navigations
self.addEventListener('fetch', event => {
  if (event.request.mode === 'navigate') {
    event.respondWith(
      fetch(event.request).catch(() => caches.match('/offline.html'))
    );
  }
});
Enter fullscreen mode Exit fullscreen mode

Background Sync

Queue failed requests and replay them when connectivity returns:

// Register sync when a request fails
async function saveData(data) {
  try {
    await fetch('/api/save', { method: 'POST', body: JSON.stringify(data) });
  } catch {
    // Store in IndexedDB for later sync
    await queueForSync(data);
    await navigator.serviceWorker.ready;
    await registration.sync.register('save-data');
  }
}

// In service worker
self.addEventListener('sync', event => {
  if (event.tag === 'save-data') {
    event.waitUntil(flushQueuedData());
  }
});
Enter fullscreen mode Exit fullscreen mode

Push Notifications

Push notifications require a subscription and a server to send them:

// Request permission and get subscription
async function subscribeToPush() {
  const registration = await navigator.serviceWorker.ready;

  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,  // required: all pushes must show a notification
    applicationServerKey: urlBase64ToUint8Array(PUBLIC_VAPID_KEY)
  });

  // Send subscription to your server
  await fetch('/api/push-subscribe', {
    method: 'POST',
    body: JSON.stringify(subscription)
  });
}

// In service worker: handle incoming push
self.addEventListener('push', event => {
  const data = event.data?.json() ?? { title: 'Update available' };
  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: '/icons/icon-192.png'
    })
  );
});
Enter fullscreen mode Exit fullscreen mode

Installation Prompt

You can intercept the browser's install prompt to show it at the right moment:

let deferredPrompt;

window.addEventListener('beforeinstallprompt', event => {
  event.preventDefault();
  deferredPrompt = event;
  document.getElementById('install-btn').hidden = false;
});

document.getElementById('install-btn').addEventListener('click', async () => {
  deferredPrompt.prompt();
  const { outcome } = await deferredPrompt.userChoice;
  console.log(`User ${outcome === 'accepted' ? 'installed' : 'dismissed'} the app`);
  deferredPrompt = null;
});
Enter fullscreen mode Exit fullscreen mode

Testing PWAs

In Firefox DevTools:

  1. Open Application tab (Firefox DevTools → Application panel)
  2. Check Service Workers: registration, status, cache storage
  3. Simulate offline: Network tab → Throttle → Offline

Lighthouse PWA audit:

npx lighthouse https://yoursite.com --categories=pwa
Enter fullscreen mode Exit fullscreen mode

Firefox PWA Support

Firefox supports service workers, web app manifests, and offline caching. As of 2023, Firefox desktop removed its PWA installation feature (the "install as app" button) on desktop platforms, though it remains in Firefox for Android.

For a purely web-based PWA without installation (just offline and caching benefits), Firefox works fully. For installable PWAs, Chrome/Edge on desktop and Firefox Android are your targets.

The Workbox library from Google simplifies service worker development significantly — worth evaluating if your caching strategy gets complex.

Top comments (0)