Gestion de cache et le fonctionnement offline avec Service Worker & Cache API

Aziz VorrezAziz Vorrez
14 min read

Vous connaissez cette page :

Quand tu n’as pas la connexion ou ta connexion est instable, Google Chome te propose de jouer au dinosore en attendant.

Mais tu t’es déjà demandé pourquoi sur certaines site qund tu n’as pas la connexion, tu as une page plus personnalisée comme :

Une page personnalisée pour le client adaptée a ta marque pour lui servir l’info dans un style assez moderne.

Et tu sais c’est comment faire pour ne pas appeler ton serveur tout les 2 secondes pour demander toujours les même chose pour plus de 2000 clients. Tu pourrais économiser plus d’argent et de ressources en faisant moin d’appels vers ton serveurs inutilement.

C’est justement a ce stade que les Services Workers et Cache API prend tout son sens et son utilisation devient rapidement une nécéssité.

Retiens ceci avant de poursuivre :

Service Worker : Est un script javascript qui :

  • s’exécute en arrière-plan ;

  • intercepte les requêtes réseau de ton site,

  • permet de répondre depuis le cache, faire des tâches offline, des notifications push, etc.

Cache API : Constitue un espace de stockage de donnée pour les Services Workers afin de stocker des requêtes HTTP et leurs réponses associées (Request/Response) dans une mémoire persistante directement accessible côté navigateur.

Alors comment tout ca fonctionne ?

1 - Services Workers :

1 - 1 - Fonctionnment d’un service worker :

Les services workers s’executent en arriere plan et leurs fonctionnement se resume en ces evenements :

  • Installation (install)

    • C'est le tout premier événement déclenché lors du téléchargement du Service Worker.

    • Objectif : préparer l'environnement du SW avant qu'il commence à fonctionner (exemple : préparer le cache, mais ici on reste théorique).

    • Si l'installation échoue (exception non gérée, promesse rejetée), le SW est abandonné.

  • Activation (activate)

    • Déclenché une fois l'installation réussie.

    • Sert à nettoyer ou mettre à jour l’environnement du SW (par exemple suppression de caches obsolètes).

    • C'est aussi ici que le SW commence réellement à "contrôler" les pages de l’application.

  • Utilisation (fetch)

    • Cet événement est déclenché pour chaque requête réseau faite par les pages contrôlées par le SW.

    • Le SW peut alors intercepter cette requête, la modifier, la bloquer ou y répondre directement.

    • C’est le cœur du comportement réseau du SW.

  • Événements additionnels :

    • push : déclenché lors de la réception d'une notification push depuis un serveur.

    • sync : déclenché lors d'une synchronisation en arrière-plan lorsque l’appareil retrouve une connexion.

    • message : permet une communication entre le thread principal (page web) et le SW.

Recapitulatif des évèvenements de Service Workers :

✅ Événement📌 But général
installPréparation avant la mise en service (préchargement, config initiale)
activateNettoyage, migration, gestion des versions précédentes
fetchInterception et gestion des requêtes réseau des pages contrôlées
pushRéception de notifications push depuis un serveur distant
syncRelance d'opérations réseau différées lors du retour de la connexion
messageCommunication entre le Service Worker et les pages clientes

État du Service Worker à chaque phase :

🕹️ État✅ Description
installingEn cours d'installation.
installedInstallé mais pas encore activé (attend la libération de l'ancien SW).
activatingEn cours d’activation.
activatedPrêt, actif, contrôle les pages ouvertes.
redundantObsolète ou rejeté par le navigateur.

1 - 2 - Cas pratique sur les Service Worker :

Faisons un cas pratique avec pour objectifs de faire une application web qui :

  • fonctionne en offline,

  • utilise le cache pour les assets,

  • intercepte les fetch pour données API,

  • affiche une page de secours offline,

  • peut gérer un message envoyé depuis la page,

  • prépare une sync background et une push notification.

1️⃣ Etape 1 : Structure du projet :

Nous allons faire le cas pratique avec projet ReactJS :

/my-react-app
│
├── public
│   ├── index.html
│   ├── offline.html           # Page de secours
│   └── service-worker.js      # Service Worker (doit être dans public)
│
├── src
│   ├── App.js                 # Composant React
│   ├── index.js               # Point d’entrée React + SW registration
│   └── ... autres fichiers
│
└── package.json

Maintenant que tu sais ou créer le fichier pour le service worker, voyons comment le configuré.

2️⃣ Etape 2 : configurons notre Service Workers :

const CACHE_NAME = 'react-app-v1';
const ASSETS = [
  '/',
  '/index.html',
  '/offline.html'
];

// INSTALL
self.addEventListener('install', event => {
  console.log('[SW] Install');
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => cache.addAll(ASSETS))
  );
});

// ACTIVATE
self.addEventListener('activate', event => {
  console.log('[SW] Activate');
  event.waitUntil(
    caches.keys().then(keys => Promise.all(
      keys.filter(key => key !== CACHE_NAME).map(key => caches.delete(key))
    ))
  );
});

// FETCH
self.addEventListener('fetch', event => {
  if (event.request.mode === 'navigate') {
    event.respondWith(
      fetch(event.request).catch(() => caches.match('/offline.html'))
    );
  }
});

// MESSAGE
self.addEventListener('message', event => {
  console.log('[SW] Message reçu :', event.data);
});

// SYNC
self.addEventListener('sync', event => {
  if (event.tag === 'sync-tag') {
    console.log('[SW] Sync background triggered');
    event.waitUntil(Promise.resolve());
  }
});

// PUSH
self.addEventListener('push', event => {
  const options = {
    body: 'Message Push reçu !',
    icon: '/logo192.png'
  };
  event.waitUntil(
    self.registration.showNotification('React PWA', options)
  );
});

Expliqu’on un peu :

🔍 1. Variables globales

const CACHE_NAME = 'react-app-v1';
const ASSETS = [
  '/',
  '/index.html',
  '/offline.html'
];

📌 Explication :

  • CACHE_NAME : Nom unique du cache pour cette version du Service Worker.
    ⚠️ Important : ce nom doit changer à chaque mise à jour (par exemple react-app-v2) pour forcer le navigateur à recréer le cache.

  • ASSETS : Liste des ressources à pré-cacher (appelé "precache") lors de l'installation. Ces fichiers seront disponibles même sans connexion réseau.


🔧 2. INSTALLATION : Préparation du cache

self.addEventListener('install', event => {
  console.log('[SW] Install');
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => cache.addAll(ASSETS))
  );
});

📌 Explication :

  • L’événement install est déclenché dès le téléchargement du Service Worker.

  • event.waitUntil() bloque l'installation jusqu'à ce que :

    • le cache soit ouvert (caches.open(CACHE_NAME)),

    • tous les fichiers dans ASSETS soient ajoutés dans le cache avec cache.addAll().

🚨 Si cette promesse échoue : le SW ne passe pas à l’étape "activate".


🔄 3. ACTIVATION : Nettoyage des anciens caches

self.addEventListener('activate', event => {
  console.log('[SW] Activate');
  event.waitUntil(
    caches.keys().then(keys => Promise.all(
      keys.filter(key => key !== CACHE_NAME).map(key => caches.delete(key))
    ))
  );
});

📌 Explication :

  • Au moment de l’activation :

    • On récupère tous les caches existants via caches.keys().

    • On supprime ceux qui ne correspondent pas au nouveau CACHE_NAME.

  • Cette étape évite l'accumulation de vieux caches inutiles (important pour les updates propres).


🌍 4. FETCH : Interception des requêtes HTTP

self.addEventListener('fetch', event => {
  if (event.request.mode === 'navigate') {
    event.respondWith(
      fetch(event.request).catch(() => caches.match('/offline.html'))
    );
  }
});

📌 Explication :

  • Le SW intercepte toutes les requêtes.

  • Filtrage : il ne gère ici que les requêtes de navigation (mode === 'navigate') : clics sur liens, chargement de pages.

  • S'il n'y a pas de connexion (ex : offline) :

    • La requête réseau échoue → il sert alors la page /offline.html depuis le cache comme "fallback".

🎯 Résultat : l'utilisateur voit une page d'erreur propre même sans connexion.


📩 5. MESSAGE : Réception de messages envoyés par la page React

self.addEventListener('message', event => {
  console.log('[SW] Message reçu :', event.data);
});

📌 Explication :

  • Permet à une page React (ou autre JS) d’envoyer des messages personnalisés au SW via :
navigator.serviceWorker.controller.postMessage('Message');
  • Très utile pour :

    • déclencher une mise à jour,

    • forcer un refresh de cache,

    • transmettre des données du client au SW.


🔄 6. SYNC : Tâches différées (Background Sync)

self.addEventListener('sync', event => {
  if (event.tag === 'sync-tag') {
    console.log('[SW] Sync background triggered');
    event.waitUntil(Promise.resolve());
  }
});

📌 Explication :

  • L'événement sync est déclenché quand la connexion Internet revient après une déconnexion.

  • Le tag permet d'identifier la tâche spécifique (ici 'sync-tag').

  • Utilisé pour :

    • envoyer des données stockées localement vers le serveur dès que le réseau revient,

    • resynchroniser des formulaires, etc.

  • event.waitUntil() assure que la tâche est terminée avant que le SW redevienne idle.


📡 7. PUSH : Réception de notifications Push

self.addEventListener('push', event => {
  const options = {
    body: 'Message Push reçu !',
    icon: '/logo192.png'
  };
  event.waitUntil(
    self.registration.showNotification('React PWA', options)
  );
});

📌 Explication :

  • Le SW capte les notifications Push envoyées par un serveur (Firebase, custom server, etc.).

  • event.waitUntil() garantit que l'affichage de la notification est terminé avant la mise en veille du SW.

  • showNotification() : méthode native pour afficher une notif système avec titre et options.


3️⃣ Etape 3 : Comment React va communiquer avec notre Service Workers :

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

// Enregistrement du Service Worker
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/service-worker.js')
      .then(reg => {
        console.log('Service Worker enregistré', reg);

        // Exemple : envoyer un message au SW
        if (reg.active) {
          reg.active.postMessage('Hello depuis React !');
        }
      })
      .catch(err => console.error('Erreur SW :', err));
  });
}

Expliquons un peu plus :

1️⃣ Importation des modules React

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

Explication :

  • React : nécessaire pour utiliser JSX (<App />).

  • ReactDOM.createRoot : méthode moderne (React 18+) pour rendre l'app React dans le DOM.

  • App : composant principal de l’application React.


2️⃣ Création de la racine React et rendu de l’application

root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

Explication :

  • Cherche l’élément DOM ayant l’ID root dans public/index.html.

  • Utilise React 18 Root API pour initialiser l’application.

  • Rendu du composant principal <App />.


3️⃣ Enregistrement du Service Worker

('serviceWorker' in navigator) {

Explication :

  • Vérifie si le navigateur prend en charge les Service Workers (obligatoire avant d'enregistrer un SW pour éviter des erreurs sur anciens navigateurs).

4️⃣ Enregistrer le Service Worker quand la page est chargée

window.addEventListener('load', () => {
    navigator.serviceWorker.register('/service-worker.js')

Explication :

  • Enregistre le Service Worker après le chargement complet de la page ('load' event).

  • /service-worker.js correspond au fichier du SW placé dans le dossier /public (obligatoire car le SW doit être à la racine du domaine).


5️⃣ Gestion de la promesse de registration

.then(reg => {
        console.log('Service Worker enregistré', reg);

Explication :

  • Si l’enregistrement réussit :

    • reg contient un ServiceWorkerRegistration.

    • Info affichée en console (utile pour déboguer).


6️⃣ Envoyer un message au Service Worker actif

if (reg.active) {
          reg.active.postMessage('Hello depuis React !');
        }

Explication :

  • Si un SW est actif (reg.active existe),

  • Envoie un message (postMessage()) au Service Worker :

    • Permet d’envoyer des données,

    • Par exemple pour déclencher un traitement dans l'événement 'message' du SW.

⚠️ Attention : si le SW n'est pas encore activé, reg.active pourrait être undefined.


7️⃣ Gestion des erreurs

      })
      .catch(err => console.error('Erreur SW :', err));

Explication :

  • Si l’enregistrement échoue (ex : mauvais chemin, contexte non HTTPS),

  • L’erreur est capturée et affichée dans la console.


4️⃣ ETAPE 4 : Mettons en place la vue client pour envoyer un message au SW :

import React from 'react';

function App() {
  const envoyerMessage = () => {
    if (navigator.serviceWorker.controller) {
      navigator.serviceWorker.controller.postMessage("Message envoyé depuis React App !");
    }
  };

  return (
    <div>
      <h1>React PWA avec Service Worker</h1>
      <button onClick={envoyerMessage}>Envoyer message au SW</button>
    </div>
  );
}

export default App;

Comment fonctionne la fonction envoyerMessage ?

Fonction envoyerMessage :

const envoyerMessage = () => {
  if (navigator.serviceWorker.controller) {
    navigator.serviceWorker.controller.postMessage("Message envoyé depuis React App !");
  }
};

✔️ Explication :

  • navigator.serviceWorker.controller :

    • Référence le Service Worker actif qui contrôle la page.

    • Si undefined, cela signifie que la page n’est pas (encore) contrôlée par un SW.

  • postMessage() :

    • Permet d’envoyer un message depuis React vers le Service Worker.

    • Ce message sera capté côté SW par l’événement :

self.addEventListener('message', event => { ... });

🔍 Utilité :

  • Transmettre une commande (ex : "update le cache"),

  • Envoyer des données locales,

  • Lancer des actions côté SW dynamiquement.

5️⃣ ETAPE 5 : Comment le Service Worker rend le message quand l’utilisateur perd la connexion :

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Offline</title></head>
<body>
  <h2>Vous êtes offline 😢</h2>
</body>
</html>

✔️ But de ce fichier :
Cette page sert d’alternative de secours (fallback) quand l'utilisateur n'a pas de connexion réseau, captée grâce à cet événement dans le Service Worker :

(event.request.mode === 'navigate') {
  event.respondWith(
    fetch(event.request).catch(() => caches.match('/offline.html'))
  );
}

Tu dois être content d’avoir obtenu ce résultat.

Maintenant nous allons simplifier un peut les choses avec Cache API.

2 - Cache API :

2 - 1 - Fonctionnment d’une Cache API :

La Cache API fait partie du standard Service Worker API.
Elle permet de stocker des réponses HTTP (fichiers, pages, requêtes API...) dans une mémoire gérée par le navigateur.
Elle est distincte de localStorage, IndexedDB : elle gère des objets de type Request / Response complets.


📦 Fonctions clés de la Cache API :

MéthodeRôle
caches.open(cacheName)Ouvre/crée un cache nommé. Renvoie une promesse avec l’objet Cache.
cache.add(request)Ajoute une ressource au cache (comme un préchargement).
cache.addAll([requests])Ajoute plusieurs ressources au cache.
cache.match(request)Cherche une ressource correspondante dans ce cache.
cache.put(request, response)Stocke une paire requête-réponse personnalisée.
cache.delete(request)Supprime une entrée spécifique.
caches.keys()Liste tous les caches existants (par nom).
caches.delete(cacheName)Supprime un cache entier.

🎯 Pourquoi utiliser Cache API dans un Service Worker ?

✔️ Permet de :

  • Stocker des assets statiques (CSS, JS, HTML),

  • Implémenter des stratégies : Cache-First, Network-First, Stale-While-Revalidate,

  • Servir des données offline ou accélérer le temps de chargement,

  • Contrôler entièrement le cache contrairement à HTTP Cache-Control (qui dépend du serveur).


1 - 2 - Cas pratique sur les Cache API :

Si nous devons utiliser Cache API pour notre service worker précédant, on aurait :

jsCopyEditconst CACHE_NAME = 'cache-api-example-v1';
const ASSETS_TO_CACHE = [
  '/',
  '/index.html',
  '/offline.html'
];

// INSTALL : Pré-caching des assets
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => cache.addAll(ASSETS_TO_CACHE))
  );
});

// ACTIVATE : Nettoyage des anciens caches
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(keys => Promise.all(
      keys.filter(key => key !== CACHE_NAME).map(key => caches.delete(key))
    ))
  );
});

// FETCH : Stratégie Cache-First pour les assets, Network-First pour les APIs
self.addEventListener('fetch', event => {
  const req = event.request;

  // Pour les requêtes API → Network-First
  if (req.url.includes('/api/')) {
    event.respondWith(
      fetch(req)
        .then(res => {
          return caches.open(CACHE_NAME).then(cache => {
            cache.put(req, res.clone()); // Stockage de la réponse API
            return res;
          });
        })
        .catch(() => caches.match(req)) // Si réseau KO → réponse en cache
    );
  } 
  // Pour tout le reste → Cache-First
  else if (req.mode === 'navigate') {
    event.respondWith(
      caches.match(req).then(cachedRes => {
        return cachedRes || fetch(req).catch(() => caches.match('/offline.html'));
      })
    );
  }
});

Voici ce qui a changé :

🔍 1. Stratégie distincte : Cache-First pour pages / assets

// FETCH : Stratégie Cache-First pour les pages et assets
self.addEventListener('fetch', event => {
  const req = event.request;

  if (req.mode === 'navigate') { // Ne cible que les navigations (pages)
    event.respondWith(
      caches.match(req).then(cachedRes => {
        return cachedRes || fetch(req).catch(() => caches.match('/offline.html'));
      })
    );
  }
});

📝 Pourquoi ?

→ Sert les pages depuis le cache si dispo (plus rapide).
→ Si pas dispo → va chercher sur le réseau.
→ Si réseau KO → renvoie /offline.html.


🔍 2. Détection et gestion spécifique des requêtes API (Network-First)

(req.url.includes('/api/')) { // Toutes les requêtes vers /api/
  event.respondWith(
    fetch(req) // Essaye d'abord le réseau
      .then(res => {
        return caches.open(CACHE_NAME).then(cache => {
          cache.put(req, res.clone()); // Stocke la réponse API pour usage futur
          return res; // Donne la réponse réseau à l'utilisateur
        });
      })
      .catch(() => caches.match(req)) // Si réseau KO → sert la réponse API du cache
  );
}

📝 Pourquoi ?

→ Donne une donnée API fraîche si réseau dispo.
→ Sinon sert la dernière réponse API du cache (offline ou si le serveur est down).


🔍 3. Mise à jour manuelle du cache pour les API (cache.put)

return caches.open(CACHE_NAME).then(cache => {
  cache.put(req, res.clone()); // On stocke explicitement la réponse dans le cache
  return res;
});

📝 Pourquoi ?

→ Assure que le cache contient toujours la dernière version de l’API.


🔍 4. Fallback propre en cas d’échec réseau

fetch(req).catch(() => caches.match('/offline.html'));

📝 Pourquoi ?

→ Si la requête échoue, l'utilisateur voit une vraie page offline.html propre
(et pas une erreur 404 ou un écran vide).


🔍 5. Nettoyage des anciens caches (Activate)

self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(keys => Promise.all(
      keys.filter(key => key !== CACHE_NAME) // On garde seulement la version actuelle
          .map(key => caches.delete(key))   // Les autres sont supprimées
    ))
  );
});

📝 Pourquoi ?

→ Évite que d’anciens caches inutiles prennent de l’espace disque.
→ Important lors des déploiements (nouvelle version du site).


🔍 6. Pré-caching (Install)

const ASSETS_TO_CACHE = [
  '/',
  '/index.html',
  '/offline.html'
];

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

📝 Pourquoi ?

→ Prépare les pages essentielles avant la première utilisation de l’app.
→ Rend l’app directement offline-ready même lors du premier lancement.


Voici quelques références utiles :
https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers

https://medium.com/@offbeatgs/optimise-api-calls-in-react-using-cache-and-service-workers-f7db2a4f5549

0
Subscribe to my newsletter

Read articles from Aziz Vorrez directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Aziz Vorrez
Aziz Vorrez