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

PWA : stratégies de cache avancées et synchronisation hors-ligne

Les service workers sont trop souvent cantonnés à un rôle passif de conteneur hors-ligne. En réalité, ils sont le cœur d'une stratégie de cache sophistiquée qui détermine si votre PWA reste fluide en conditions réseau dégradées ou s'effondre à la première micro-coupure. La différence entre une PWA crédible et un gadget réside dans la granularité des décisions de cache : quelles ressources mettre en cache immédiatement, lesquelles laisser au réseau, et surtout, comment arbitrer entre fraîcheur et disponibilité quand la connexion vacille.

Stratégies de cache : au-delà du tout-ou-rien

Cache-First (cache, falling back to network) convient aux ressources statiques qui changent rarement : assets applicatifs, polices, images. Vous interceptez la requête dans le service worker, consultez le cache en priorité, et ne faites appel au réseau que si la ressource n'existe pas ou a expiré. Cette stratégie minimise la latence mais risque de servir du contenu obsolète si vous n'implémentez pas une invalidation intelligente. Network-First (network, falling back to cache) s'impose pour les données dynamiques : appels API, contenus actualisés. Vous tentez d'abord le réseau, ce qui garantit la fraîcheur, mais basculez sur le cache en cas de timeout ou d'erreur réseau. C'est le compromis le plus équilibré pour les applications de contenu ou les dashboards. Stale-While-Revalidate est le pattern le plus sophistiqué : vous servez immédiatement la version en cache (même obsolète) à l'utilisateur, puis mettez à jour le cache en arrière-plan sans bloquer l'interaction. L'utilisateur ne subit jamais le délai réseau, mais reçoit une notification discrète quand une version plus fraîche arrive.

Voici un exemple concret d'orchestration multi-stratégies dans un service worker :

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

  // Assets statiques : 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 avec 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()));
            // Notifier l'app qu'une nouvelle version est dispo
            self.clients.matchAll().then(clients => {
              clients.forEach(client => {
                client.postMessage({ type: 'CACHE_UPDATED', url: event.request.url });
              });
            });
            return response;
          });
          return cached || fetchPromise;
        })
    );
  }
});

La clé est de différencier les ressources par type et durée de vie. Les assets compilés (JS, CSS) sont stables : Cache-First avec un numéro de version suffit. Les API sont volatiles : Network-First avec fallback en cache est obligatoire pour ne pas servir de données périmées en offline. L'HTML applicatif bénéficie de Stale-While-Revalidate car les utilisateurs acceptent une version légèrement décalée pourvu qu'elle charge instantanément.

Background Sync et Periodic Sync : la synchronisation invisible

Network-First résout le problème de la lecture hors-ligne, mais que faire quand l'utilisateur tente d'écrire (soumettre un formulaire, uploader une image) en 4G défaillante ? La Background Sync API permet d'enregistrer l'intention de l'utilisateur dans IndexedDB, d'afficher un statut « En attente de synchronisation » dans l'UI, et de rejouer l'opération dès que la connexion revient. Contrairement à une simple retry loop côté client (qui s'arrête si l'app ferme), Background Sync persiste même après fermeture du navigateur.

L'implémentation repose sur l'événement sync du service worker. Vous enregistrez une synchronisation avec une étiquette unique, et le navigateur (ou l'OS, via Capacitor sur mobile) déclenche le handler dès que le réseau est disponible. Periodic Sync va plus loin : il permet de mettre à jour le cache à intervalles réguliers (toutes les heures, chaque matin) même si l'app n'est pas active. Utile pour les news apps, les dashboards, ou tout contenu qui doit rester frais sans interaction utilisateur.

// Enregistrer une synchronisation lors d'une soumission 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);
      // Sauvegarder en IndexedDB en cas d'échec réseau
      await saveToIndexedDB('pending-submissions', formData);
      try {
        await fetch('/api/submit', { method: 'POST', body: formData });
        form.reset();
      } catch (error) {
        // Enregistrer une sync tag
        registration.sync.register('submit-form');
        showNotification('Sera envoyé quand le réseau revient');
      }
    });
  });
}

// Dans le 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))
          )
        );
      })
    );
  }
});

Notez que Background Sync nécessite que le service worker soit enregistré et que l'utilisateur ait accordé la permission (implicite sur certains navigateurs). Sur web, le support varie (excellent sur Chrome/Edge, limité sur Safari). Sur mobile via Capacitor, c'est plus fiable grâce à l'intégration OS.

Pièges courants : cache poisoning et versioning

Le piège le plus courant est la pollution du cache : une version corrompue ou une API qui retourne une erreur 500 est mise en cache, puis resservie indéfiniment. Toujours implémenter une validation au moment du cache : vérifiez le status HTTP (200-299), inspectez le Content-Type, et rejetez les réponses vides ou mal formées. Utilisez des étiquettes de version explicites (v1-assets, v2-api) et nettoyez les anciennes versions lors de la mise à jour du service worker.

Deuxième piège : oublier que le cache ne se met pas à jour tout seul. Stale-While-Revalidate est séduisant, mais exige une UI informant l'utilisateur qu'une nouvelle version est disponible (via postMessage du service worker). Sans cela, l'utilisateur croit qu'il a la dernière version alors qu'il navigue une copie du jour précédent. Implémentez une notification discrète ou un bouton « Actualiser » qui force un rechargement complet.

Troisième piège : négliger l'IndexedDB pour les données complexes. Si vous synchronisez en arrière-plan, le simple localStorage est insuffisant pour persister les uploads, les formulaires longs, ou les listes de modifications. IndexedDB offre bien plus de capacité (jusqu'à plusieurs centaines de MB) et une API transactionnelle. Couplée à Background Sync, c'est la fondation d'une synchronisation résiliente.

Conclusion opérationnelle

Une PWA robuste ne se limite pas à fonctionner offline ; elle anticipe les conditions réseau dégradées et arbitre intelligemment entre fraîcheur et disponibilité. Implémentez une stratégie de cache granulaire (Cache-First pour assets, Network-First pour API, Stale-While-Revalidate pour HTML), puis ajoutez Background Sync pour les écritures. Testez systématiquement en 3G throttled (DevTools) et en mode offline. Versionnez vos caches, nettoyez les anciennes versions, et validez les réponses avant de les stocker. Enfin, communiquez à l'utilisateur l'état de la synchronisation : « En attente de réseau », « Mis à jour », « Erreur ». Ces détails transforment une PWA de prototype fragile en application de production crédible, utilisable même en métro parisien bondé.

Développeur Angular & Mobile freelance — Strasbourg.

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