Sachith Dassanayake Software Engineering Offline‑first PWAs: caching & background sync — Cheat Sheet — Practical Guide (Oct 24, 2025)

Offline‑first PWAs: caching & background sync — Cheat Sheet — Practical Guide (Oct 24, 2025)

Offline‑first PWAs: caching & background sync — Cheat Sheet — Practical Guide (Oct 24, 2025)

Offline‑first PWAs: caching & background sync — Cheat Sheet

Offline‑first PWAs: caching & background sync — Cheat Sheet

Level: Intermediate

October 24, 2025

Introduction

Progressive Web Apps (PWAs) that work offline or under flaky network conditions require a robust strategy combining caching with background synchronisation. This cheat sheet walks you through modern offline-first architecture, focusing on practical application with stable API features available as of late 2025, applicable primarily in Chromium-based browsers (v110+) and Firefox (v115+). Safari support is improving but with known limitations in background sync as of this writing.

Prerequisites

  • Knowledge of Service Workers: installation, activation, fetch event handling.
  • HTTPS hosting (mandatory for Service Workers and Background Sync).
  • Familiarity with Cache Storage API for resource caching.
  • Understanding of IndexedDB for persistent storage beyond caches (optional but recommended).
  • Browser support awareness: Background Sync API is stable on Chromium and Firefox; Apple platforms (Safari, iOS) have limited or no support as of 2025.

Hands-on steps

1. Register a Service Worker

Start by registering your service worker in your main JS entry point:


// Register the service worker
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(reg => console.log('SW registered, scope:', reg.scope))
    .catch(err => console.error('SW registration failed:', err));
}

2. Cache essential resources during installation

Use the service worker’s install event to pre-cache app shell assets. This ensures core files are available offline immediately.


// sw.js
const CACHE_NAME = 'app-shell-v1';
const APP_SHELL = [
  '/',
  '/index.html',
  '/styles.css',
  '/main.js',
  '/fallback.html'
];

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

3. Serve cached files with a cache-first strategy

Intercept fetch requests and respond with cached assets when available. Fall back to network for others.


self.addEventListener('fetch', event => {
  const request = event.request;

  // Only handle GET requests
  if (request.method !== 'GET') return;

  event.respondWith(
    caches.match(request).then(cachedResponse => {
      if (cachedResponse) {
        return cachedResponse;
      }
      return fetch(request).catch(() => {
        // Optional: serve fallback content, e.g. fallback.html for navigation requests
        if (request.mode === 'navigate') {
          return caches.match('/fallback.html');
        }
      });
    })
  );
});

4. Use Background Sync to retry failed requests

When a user is offline, POST/PUT/DELETE requests can be saved locally and retried when the connection is restored using Background Sync.

Queue failed requests in IndexedDB

On fetch failures for mutating API calls, save requests to IndexedDB. The code below uses a simplified example.


// Helper function to save requests to IndexedDB (simplified)
async function saveFailedRequest(request) {
  const db = await openDB('requests-db', 1, {
    upgrade(db) {
      db.createObjectStore('outbox', { autoIncrement: true });
    },
  });

  const reqClone = request.clone();
  const body = await reqClone.text();

  await db.put('outbox', {
    url: reqClone.url,
    method: reqClone.method,
    headers: [...reqClone.headers],
    body,
  });
}

Register Sync in the page or service worker


// Typically in client code, register sync after failed network request
if ('serviceWorker' in navigator && 'SyncManager' in window) {
  navigator.serviceWorker.ready.then(swRegistration => {
    return swRegistration.sync.register('retry-outbox');
  });
}

Handle Sync event in service worker

Process the queue on sync:


self.addEventListener('sync', event => {
  if (event.tag === 'retry-outbox') {
    event.waitUntil(processOutbox());
  }
});

async function processOutbox() {
  const db = await openDB('requests-db', 1);
  const tx = db.transaction('outbox', 'readwrite');
  const store = tx.objectStore('outbox');
  const allRequests = await store.getAll();

  for (const savedRequest of allRequests) {
    const { url, method, headers, body } = savedRequest;

    try {
      const response = await fetch(url, {
        method,
        headers: new Headers(headers),
        body: body.length ? body : undefined,
      });

      if (response.ok) {
        // Remove request from IndexedDB after success
        const key = await store.getKey(savedRequest);
        await store.delete(key);
      }
    } catch (err) {
      // Network failure; keep in queue for next sync
    }
  }
  await tx.done;
}

Common pitfalls

  • Unbounded cache growth: Use cache versioning and clean-up old caches in activate event.
  • Ignoring failed cache.put: Always handle promises; cache writes can fail due to quota limits.
  • Background Sync not supported everywhere: Detect support and provide fallback UI, e.g., “retry” buttons.
  • Synchronous IndexedDB usage: All IDB calls are async; misuse may cause hard-to-debug race conditions.
  • Not responding to navigation fallback: Users offline should receive meaningful fallback page, not network errors.

Validation

  • Test offline behaviour: Use browser dev tools to simulate offline mode, check app shell loads and API calls queue.
  • Check service worker lifecycle: Inspect chrome://serviceworker-internals/ or browser-specific devtools for registration status.
  • Background Sync trigger: Force offline, submit requests, reconnect and verify sync event processes the queue.
  • Storage usage monitoring: Inspect Cache Storage and IndexedDB in devtools for expected entries.
  • Network requests: Confirm retries do not duplicate or lose requests.

Checklist / TL;DR

  • ✅ Serve a minimal app shell from cache with Cache Storage API
  • ✅ Use fetch event handler with cache-first strategy for static resources
  • ✅ Queue failing mutating requests (POST/PUT/DELETE) in IndexedDB
  • ✅ Register and handle Background Sync events to replay queued requests
  • ✅ Test offline, online recovery, and cache version management regularly
  • ⚠️ Feature detection for Background Sync; provide UI fallbacks for unsupported browsers
  • ⚠️ Clean up old caches on activation to prevent storage bloat

When to choose cache-first vs network-first?

  • Cache-first (offline‑first): Best for app shells, static assets, or read-only content where immediate availability is critical and stale data is acceptable briefly.
  • Network-first: Better for frequently updated, user-sensitive content (e.g., news feeds, API data). Use with fallback caching if offline.

References

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Related Post