← Retour au blog
PWAservice-workersoffline-firstcache-strategiesbackground-sync

PWA: Advanced Caching Strategies and Offline Synchronization

Service workers are too often relegated to a passive offline container role. In reality, they are the heart of a sophisticated caching strategy that determines whether your PWA remains fluid under degraded network conditions or collapses at the first micro-outage. The difference between a credible PWA and a gimmick lies in the granularity of caching decisions: which resources to cache immediately, which to leave to the network, and crucially, how to arbitrate between freshness and availability when connectivity falters.

Cache Strategies: Beyond All-or-Nothing

Cache-First (cache, falling back to network) suits static resources that change rarely: application assets, fonts, images. You intercept the request in the service worker, consult the cache first, and only resort to the network if the resource doesn't exist or has expired. This strategy minimizes latency but risks serving stale content if you don't implement intelligent invalidation. Network-First (network, falling back to cache) is essential for dynamic data: API calls, updated content. You attempt the network first, guaranteeing freshness, but fall back to cache on timeout or network error. It's the most balanced compromise for content applications or dashboards. Stale-While-Revalidate is the most sophisticated pattern: you immediately serve the cached version (even if stale) to the user, then update the cache in the background without blocking interaction. The user never experiences network latency, but receives a discreet notification when a fresher version arrives.

Here's a concrete example of multi-strategy orchestration in a service worker:

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

  // Static assets: Cache-First
  if (url.pathname.match(/\.(js|css|png|woff2)$/)) {
    event.respondWith(
      caches.match(event.request)
        .then(response => response || fetch(event.request)
          .then(resp => {
            const cache = caches.open('v1-assets');
            cache.then(c => c.put(event.request, resp.clone()));
            return resp;
          })
        )
    );
  }

  // API: Network-First with timeout
  if (url.pathname.startsWith('/api/')) {
    event.respondWith(
      Promise.race([
        fetch(event.request),
        new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000))
      ])
        .then(response => {
          const cache = caches.open('v1-api');
          cache.then(c => c.put(event.request, response.clone()));
          return response;
        })
        .catch(() => caches.match(event.request)
          .then(cached => cached || new Response('Offline', { status: 503 }))
        )
    );
  }

  // HTML: Stale-While-Revalidate
  if (event.request.mode === 'navigate') {
    event.respondWith(
      caches.match(event.request)
        .then(cached => {
          const fetchPromise = fetch(event.request).then(response => {
            const cache = caches.open('v1-html');
            cache.then(c => c.put(event.request, response.clone()));
            // Notify app that a new version is available
            self.clients.matchAll().then(clients => {
              clients.forEach(client => {
                client.postMessage({ type: 'CACHE_UPDATED', url: event.request.url });
              });
            });
            return response;
          });
          return cached || fetchPromise;
        })
    );
  }
});

The key is to differentiate resources by type and lifespan. Compiled assets (JS, CSS) are stable: Cache-First with a version number suffices. APIs are volatile: Network-First with cache fallback is mandatory to avoid serving stale data offline. Application HTML benefits from Stale-While-Revalidate because users accept a slightly delayed version provided it loads instantly.

Background Sync and Periodic Sync: Invisible Synchronization

Network-First solves the offline reading problem, but what happens when the user attempts to write (submit a form, upload an image) on failing 4G? The Background Sync API allows you to register the user's intent in IndexedDB, display a "Pending synchronization" status in the UI, and replay the operation as soon as connectivity returns. Unlike a simple retry loop on the client (which stops if the app closes), Background Sync persists even after the browser closes.

The implementation relies on the sync event in the service worker. You register a synchronization with a unique tag, and the browser (or OS via Capacitor on mobile) triggers the handler as soon as the network becomes available. Periodic Sync goes further: it allows you to update the cache at regular intervals (every hour, each morning) even if the app isn't active. Useful for news apps, dashboards, or any content that must stay fresh without user interaction.

// Register synchronization when submitting offline
if ('serviceWorker' in navigator && 'SyncManager' in window) {
  navigator.serviceWorker.ready.then(registration => {
    form.addEventListener('submit', async (e) => {
      e.preventDefault();
      const formData = new FormData(form);
      // Save to IndexedDB in case of network failure
      await saveToIndexedDB('pending-submissions', formData);
      try {
        await fetch('/api/submit', { method: 'POST', body: formData });
        form.reset();
      } catch (error) {
        // Register a sync tag
        registration.sync.register('submit-form');
        showNotification('Will be sent when network returns');
      }
    });
  });
}

// In the service worker
self.addEventListener('sync', event => {
  if (event.tag === 'submit-form') {
    event.waitUntil(
      getAllFromIndexedDB('pending-submissions').then(submissions => {
        return Promise.all(
          submissions.map(sub => 
            fetch('/api/submit', { method: 'POST', body: sub })
              .then(() => deleteFromIndexedDB('pending-submissions', sub.id))
          )
        );
      })
    );
  }
});

Note that Background Sync requires the service worker to be registered and the user to have granted permission (implicit on some browsers). On the web, support varies (excellent on Chrome/Edge, limited on Safari). On mobile via Capacitor, it's more reliable thanks to OS integration.

Common Pitfalls: Cache Poisoning and Versioning

The most common pitfall is cache pollution: a corrupted version or an API returning a 500 error is cached, then resserved indefinitely. Always implement validation at cache time: check HTTP status (200-299), inspect Content-Type, and reject empty or malformed responses. Use explicit version tags (v1-assets, v2-api) and clean up old versions when updating the service worker.

Second pitfall: forgetting that the cache doesn't update itself. Stale-While-Revalidate is seductive, but demands UI informing the user that a new version is available (via postMessage from the service worker). Without this, the user believes they have the latest version while navigating a copy from the previous day. Implement a discreet notification or a "Refresh" button that forces a full reload.

Third pitfall: neglecting IndexedDB for complex data. If you're synchronizing in the background, plain localStorage is insufficient to persist uploads, lengthy forms, or lists of changes. IndexedDB offers far more capacity (up to several hundred MB) and a transactional API. Coupled with Background Sync, it's the foundation of resilient synchronization.

Operational Conclusion

A robust PWA doesn't merely work offline; it anticipates degraded network conditions and intelligently arbitrates between freshness and availability. Implement a granular cache strategy (Cache-First for assets, Network-First for APIs, Stale-While-Revalidate for HTML), then add Background Sync for writes. Systematically test on throttled 3G (DevTools) and offline mode. Version your caches, clean up old versions, and validate responses before storing them. Finally, communicate synchronization status to the user: "Waiting for network", "Updated", "Error". These details transform a PWA from a fragile prototype into a credible production application, usable even on a packed Parisian metro.

Développeur Angular & Mobile freelance — Strasbourg.

© 2026 Emilien Pons — Tous droits réservés.Conçu avec Angular, PrimeNG et ❤️