Stratégies de mise en cache des service workers

Jusqu'à présent, il n'y a eu que des mentions et de minuscules extraits de code de l'interface Cache. Pour utiliser efficacement les service workers, il est nécessaire d'adopter une ou plusieurs stratégies de mise en cache, ce qui nécessite une certaine connaissance de l'interface Cache.

Une stratégie de mise en cache est une interaction entre l'événement fetch d'un service worker et l'interface Cache. La manière dont une stratégie de mise en cache est écrite dépend. Par exemple, il peut être préférable de traiter les requêtes pour les éléments statiques différemment des documents, ce qui affecte la composition d'une stratégie de mise en cache.

Avant d'examiner les stratégies, prenons une seconde pour voir ce qu'est l'interface Cache et ce qu'elle est, et un bref aperçu de certaines des méthodes qu'elle propose pour gérer les caches des service workers.

Interface Cache et cache HTTP

Si vous n'avez jamais travaillé avec l'interface Cache, il peut être tentant de la considérer comme identique, ou du moins liée au cache HTTP. Ce n'est pas le cas.

  • L'interface Cache est un mécanisme de mise en cache entièrement distinct du cache HTTP.
  • Quelle que soit la configuration de Cache-Control utilisée pour influer sur le cache HTTP, les éléments stockés dans l'interface Cache n'ont aucune incidence.

Il est utile de considérer les caches des navigateurs comme étant superposés. Le cache HTTP est un cache de bas niveau alimenté par des paires clé/valeur et des directives exprimées dans des en-têtes HTTP.

En revanche, l'interface Cache est un cache de haut niveau géré par une API JavaScript. Cela offre plus de flexibilité que lors de l'utilisation de paires clé-valeur HTTP relativement simplistes, et représente la moitié de ce qui rend les stratégies de mise en cache possibles. Voici quelques méthodes d'API importantes pour les caches de service worker:

  • CacheStorage.open pour créer une instance Cache.
  • Cache.add et Cache.put pour stocker les réponses réseau dans un cache de service worker.
  • Cache.match pour localiser une réponse mise en cache dans une instance Cache.
  • Cache.delete pour supprimer une réponse mise en cache d'une instance Cache.

En voici quelques-uns. Il existe d'autres méthodes utiles, mais ce sont les méthodes de base que vous utiliserez plus loin dans ce guide.

L'événement simple fetch

L'autre moitié d'une stratégie de mise en cache est l'événement fetch du service worker. Jusqu'à présent, dans cette documentation, vous avez entendu parler de l'"interception des requêtes réseau", et c'est dans l'événement fetch d'un service worker que cela se produit:

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('install', (event) => {
  event.waitUntil(caches.open(cacheName));
});

self.addEventListener('fetch', async (event) => {
  // Is this a request for an image?
  if (event.request.destination === 'image') {
    // Open the cache
    event.respondWith(caches.open(cacheName).then((cache) => {
      // Respond with the image from the cache or from the network
      return cache.match(event.request).then((cachedResponse) => {
        return cachedResponse || fetch(event.request.url).then((fetchedResponse) => {
          // Add the network response to the cache for future visits.
          // Note: we need to make a copy of the response to save it in
          // the cache and use the original as the request response.
          cache.put(event.request, fetchedResponse.clone());

          // Return the network response
          return fetchedResponse;
        });
      });
    }));
  } else {
    return;
  }
});

Il s'agit d'un exemple jouet, que vous pouvez voir en action par vous-même, mais il offre un aperçu de ce que les service workers peuvent faire. Le code ci-dessus effectue les opérations suivantes:

  1. Inspectez la propriété destination de la requête pour voir s'il s'agit d'une requête d'image.
  2. Si l'image se trouve dans le cache du service worker, diffusez-la à partir de cet emplacement. Si ce n'est pas le cas, récupérez l'image à partir du réseau, stockez la réponse dans le cache et renvoyez-la.
  3. Toutes les autres requêtes sont transmises via le service worker sans interaction avec le cache.

L'objet event d'une extraction contient une propriété request comprenant des informations utiles pour vous aider à identifier le type de chaque requête:

  • url, qui correspond à l'URL de la requête réseau actuellement gérée par l'événement fetch.
  • method, qui est la méthode de requête (par exemple, GET ou POST).
  • mode, qui décrit le mode de la requête. La valeur 'navigate' permet souvent de distinguer les requêtes de documents HTML des autres demandes.
  • destination, qui décrit le type de contenu demandé de manière à éviter d'utiliser l'extension de fichier de l'élément demandé.

Là encore, "asynchrony" s'appelle le jeu. N'oubliez pas que l'événement install propose une méthode event.waitUntil qui accepte une promesse et attend qu'elle soit résolue avant de passer à l'activation. L'événement fetch propose une méthode event.respondWith similaire que vous pouvez utiliser pour renvoyer le résultat d'une requête fetch asynchrone ou une réponse renvoyée par la méthode match de l'interface Cache.

Stratégies de mise en cache

Maintenant que vous connaissez les instances Cache et le gestionnaire d'événements fetch, vous êtes prêt à vous plonger dans les stratégies de mise en cache des service workers. Bien que les possibilités soient pratiquement infinies, ce guide s'appuie sur les stratégies proposées avec Workbox. Vous pouvez ainsi vous faire une idée de ce qui se passe en interne dans Workbox.

Cache uniquement

Affiche le flux entre la page, le service worker et le cache.

Commençons par une stratégie de mise en cache simple que nous appellerons "Cache uniquement". En effet, lorsque le service worker contrôle la page, les requêtes correspondantes n'arrivent que dans le cache. Cela signifie que tous les éléments mis en cache devront être mis en pré-cache pour que le modèle fonctionne et que ces éléments ne seront jamais mis à jour dans le cache tant que le service worker n'aura pas été mis à jour.

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

// Assets to precache
const precachedAssets = [
  '/possum1.jpg',
  '/possum2.jpg',
  '/possum3.jpg',
  '/possum4.jpg'
];

self.addEventListener('install', (event) => {
  // Precache assets on install
  event.waitUntil(caches.open(cacheName).then((cache) => {
    return cache.addAll(precachedAssets);
  }));
});

self.addEventListener('fetch', (event) => {
  // Is this one of our precached assets?
  const url = new URL(event.request.url);
  const isPrecachedRequest = precachedAssets.includes(url.pathname);

  if (isPrecachedRequest) {
    // Grab the precached asset from the cache
    event.respondWith(caches.open(cacheName).then((cache) => {
      return cache.match(event.request.url);
    }));
  } else {
    // Go to the network
    return;
  }
});

Ci-dessus, un tableau d'éléments est mis en pré-cache au moment de l'installation. Lorsque le service worker gère des récupérations, nous vérifions si l'URL de requête gérée par l'événement fetch se trouve dans le tableau d'éléments en pré-cache. Si tel est le cas, nous récupérons la ressource dans le cache et ignorons le réseau. Les autres requêtes sont transmises au réseau, et uniquement au réseau. Pour voir cette stratégie en action, regardez cette démonstration avec votre console ouverte.

Réseau uniquement

Affiche le flux entre la page, le service worker et le réseau.

Le contraire de "Cache uniquement" est "Réseau uniquement", où une requête est transmise via un service worker au réseau sans aucune interaction avec le cache du service worker. Il s'agit d'une bonne stratégie pour assurer l'actualisation du contenu (pensez au balisage), mais en contrepartie, cela ne fonctionnera jamais lorsque l'utilisateur est hors connexion.

Pour s'assurer qu'une requête passe par le réseau, vous n'appelez pas event.respondWith pour une requête correspondante. Pour être explicite, vous pouvez ajouter un return; vide dans votre rappel d'événement fetch pour les requêtes que vous souhaitez transmettre au réseau. C'est ce qui se passe dans la version de démonstration de la stratégie"Cache uniquement" pour les requêtes qui ne sont pas mises en pré-cache.

Mettre d'abord le cache en cache, en revenant sur le réseau

Affiche le flux depuis la page, vers le service worker, le cache, puis le réseau s'il ne se trouve pas dans le cache.

Avec cette stratégie, les choses deviennent un peu plus compliquées. Pour les demandes avec correspondance, le processus est le suivant:

  1. La requête frappe le cache. Si l'élément se trouve dans le cache, diffusez-le à partir de celui-ci.
  2. Si la requête ne figure pas dans le cache, accédez au réseau.
  3. Une fois la requête réseau terminée, ajoutez-la au cache, puis renvoyez la réponse du réseau.

Voici un exemple de cette stratégie, que vous pouvez tester dans une démonstration en direct:

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('fetch', (event) => {
  // Check if this is a request for an image
  if (event.request.destination === 'image') {
    event.respondWith(caches.open(cacheName).then((cache) => {
      // Go to the cache first
      return cache.match(event.request.url).then((cachedResponse) => {
        // Return a cached response if we have one
        if (cachedResponse) {
          return cachedResponse;
        }

        // Otherwise, hit the network
        return fetch(event.request).then((fetchedResponse) => {
          // Add the network response to the cache for later visits
          cache.put(event.request, fetchedResponse.clone());

          // Return the network response
          return fetchedResponse;
        });
      });
    }));
  } else {
    return;
  }
});

Bien que cet exemple ne couvre que les images, il s'agit d'une excellente stratégie à appliquer à tous les éléments statiques (tels que CSS, JavaScript, images et polices), en particulier ceux dont la version est hachée. Elle accélère le lancement des éléments immuables en ignorant toute vérification d'actualisation du contenu auprès du serveur que le cache HTTP peut lancer. Plus important encore, tous les éléments mis en cache seront disponibles hors connexion.

Le réseau d'abord, avec le cache

Affiche le flux depuis la page, vers le service worker, vers le réseau, puis vers le cache si le réseau n'est pas disponible.

Si vous inversez la stratégie "Mise en cache d'abord, réseau en second", vous vous retrouvez avec la stratégie "Réseau d'abord, mise en cache en second", comme son nom l'indique:

  1. Vous allez d'abord sur le réseau pour une requête et placez la réponse dans le cache.
  2. Si vous vous déconnectez plus tard, vous revenez à la dernière version de cette réponse dans le cache.

Cette stratégie est idéale pour les requêtes HTML ou API lorsque, en ligne, vous souhaitez obtenir la version la plus récente d'une ressource, mais pas accorder un accès hors connexion à la version disponible la plus récente. Voici ce à quoi cela pourrait ressembler lorsqu'il est appliqué aux demandes pour HTML:

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('fetch', (event) => {
  // Check if this is a navigation request
  if (event.request.mode === 'navigate') {
    // Open the cache
    event.respondWith(caches.open(cacheName).then((cache) => {
      // Go to the network first
      return fetch(event.request.url).then((fetchedResponse) => {
        cache.put(event.request, fetchedResponse.clone());

        return fetchedResponse;
      }).catch(() => {
        // If the network is unavailable, get
        return cache.match(event.request.url);
      });
    }));
  } else {
    return;
  }
});

Vous pouvez essayer dans une démonstration. Tout d'abord, accédez à la page. Vous devrez peut-être actualiser la page avant de placer la réponse HTML dans le cache. Ensuite, dans vos outils de développement, simulez une connexion hors connexion, puis actualisez à nouveau la page. La dernière version disponible sera diffusée instantanément à partir du cache.

Dans les situations où la capacité hors connexion est importante, mais que vous devez équilibrer cette capacité avec l'accès à la version la plus récente d'un peu de balisage ou de données d'API, une stratégie efficace pour atteindre cet objectif consiste à mettre en place une stratégie "réseau d'abord, puis mise en cache".

Obsolète lors de la revalidation

Affiche le flux depuis la page, vers le service worker, le cache, puis le réseau vers le cache.

Parmi les stratégies présentées jusqu'à présent, l'option "Obsolète et revalidée" est la plus complexe. Elle est semblable aux deux dernières stratégies d'une certaine manière, mais la procédure donne la priorité à la vitesse d'accès à une ressource, tout en la maintenant à jour en arrière-plan. Voici à quoi ressemble cette stratégie:

  1. Lors de la première requête d'élément, récupérez-le sur le réseau, placez-le dans le cache et renvoyez la réponse du réseau.
  2. Lors des requêtes suivantes, diffusez d'abord l'élément à partir du cache, puis "en arrière-plan", demandez-le à nouveau au réseau et mettez à jour l'entrée de cache de l'élément.
  3. Pour les requêtes ultérieures, vous recevrez la dernière version extraite du réseau placée dans le cache lors de l'étape précédente.

Il s'agit d'une excellente stratégie pour les choses très importantes à garder à jour, mais pas essentielles. Pensez à des choses comme des avatars pour un site de médias sociaux. Elles sont mises à jour au fur et à mesure que les utilisateurs le font, mais la dernière version n'est pas strictement nécessaire à chaque demande.

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('fetch', (event) => {
  if (event.request.destination === 'image') {
    event.respondWith(caches.open(cacheName).then((cache) => {
      return cache.match(event.request).then((cachedResponse) => {
        const fetchedResponse = fetch(event.request).then((networkResponse) => {
          cache.put(event.request, networkResponse.clone());

          return networkResponse;
        });

        return cachedResponse || fetchedResponse;
      });
    }));
  } else {
    return;
  }
});

Vous pourrez voir cela en action dans encore une autre démonstration en direct, en particulier si vous prêtez attention à l'onglet "Network" (Réseau) des outils pour les développeurs de votre navigateur, ainsi qu'à la visionneuse CacheStorage (si les outils pour les développeurs de votre navigateur disposent d'un tel outil).

En avant pour Workbox !

Ce document conclut notre examen de l'API des service workers et des API associées. Vous en savez donc suffisamment sur l'utilisation directe des service workers pour commencer à bricoler Workbox.