Accélérer le service worker grâce aux préchargements de navigation

La préchargement de navigation vous permet de surmonter le temps de démarrage du service worker en effectuant des requêtes en parallèle.

Jake Archibald
Jake Archibald

Browser Support

  • Chrome: 59.
  • Edge: 18.
  • Firefox: 99.
  • Safari: 15.4.

Source

Résumé

Problème

Lorsque vous accédez à un site qui utilise un service worker pour gérer les événements de récupération, le navigateur demande une réponse au service worker. Cela implique de démarrer le service worker (s'il n'est pas déjà en cours d'exécution) et de distribuer l'événement de récupération.

Le temps de démarrage dépend de l'appareil et des conditions. Elle est généralement d'environ 50 ms. Sur mobile, il est plutôt de 250 ms. Dans les cas extrêmes (appareils lents, CPU en difficulté), il peut dépasser 500 ms. Toutefois, comme le service worker reste actif pendant une durée déterminée par le navigateur entre les événements, vous ne subissez ce délai qu'occasionnellement, par exemple lorsque l'utilisateur accède à votre site depuis un nouvel onglet ou un autre site.

Le temps de démarrage n'est pas un problème si vous répondez à partir du cache, car l'avantage de contourner le réseau est supérieur au délai de démarrage. Mais si vous répondez en utilisant le réseau…

Démarrage du logiciel
Demande de navigation

La requête réseau est retardée par le démarrage du service worker.

Nous continuons de réduire le temps de démarrage en utilisant la mise en cache du code dans V8, en ignorant les service workers qui n'ont pas d'événement "fetch", en lançant les service workers de manière spéculative et en effectuant d'autres optimisations. Toutefois, le temps de démarrage sera toujours supérieur à zéro.

Facebook a attiré notre attention sur l'impact de ce problème et nous a demandé de trouver un moyen d'effectuer des requêtes de navigation en parallèle :

Démarrage du logiciel
Demande de navigation

Le préchargement de la navigation à la rescousse

Le préchargement de navigation est une fonctionnalité qui vous permet de dire : "Lorsque l'utilisateur effectue une requête de navigation GET, démarrez la requête réseau pendant que le service worker démarre".

Le délai de démarrage est toujours présent, mais il ne bloque pas la requête réseau. L'utilisateur obtient donc le contenu plus rapidement.

Voici une vidéo de son fonctionnement, où le service worker reçoit un délai de démarrage délibéré de 500 ms à l'aide d'une boucle while :

Voici la démo. Pour bénéficier des avantages du préchargement de navigation, vous devez disposer d'un navigateur compatible.

Activer le préchargement de la navigation

addEventListener('activate', event => {
  event.waitUntil(async function() {
    // Feature-detect
    if (self.registration.navigationPreload) {
      // Enable navigation preloads!
      await self.registration.navigationPreload.enable();
    }
  }());
});

Vous pouvez appeler navigationPreload.enable() quand vous le souhaitez ou le désactiver avec navigationPreload.disable(). Toutefois, comme votre événement fetch doit l'utiliser, il est préférable de l'activer et de le désactiver dans l'événement activate de votre service worker.

Utiliser la réponse préchargée

Le navigateur effectue désormais des préchargements pour les navigations, mais vous devez toujours utiliser la réponse :

addEventListener('fetch', event => {
  event.respondWith(async function() {
    // Respond from the cache if we can
    const cachedResponse = await caches.match(event.request);
    if (cachedResponse) return cachedResponse;

    // Else, use the preloaded response, if it's there
    const response = await event.preloadResponse;
    if (response) return response;

    // Else try the network.
    return fetch(event.request);
  }());
});

event.preloadResponse est une promesse qui se résout avec une réponse, si :

  • Le préchargement de navigation est activé.
  • La requête est une requête GET.
  • La requête est une requête de navigation (que les navigateurs génèrent lorsqu'ils chargent des pages, y compris des iFrames).

Sinon, event.preloadResponse est toujours là, mais il est résolu avec undefined.

Si votre page a besoin de données provenant du réseau, le moyen le plus rapide consiste à les demander dans le service worker et à créer une réponse unique en flux continu contenant des parties du cache et des parties du réseau.

Disons que nous voulons afficher un article :

addEventListener('fetch', event => {
  const url = new URL(event.request.url);
  const includeURL = new URL(url);
  includeURL.pathname += 'include';

  if (isArticleURL(url)) {
    event.respondWith(async function() {
      // We're going to build a single request from multiple parts.
      const parts = [
        // The top of the page.
        caches.match('/article-top.include'),
        // The primary content
        fetch(includeURL)
          // A fallback if the network fails.
          .catch(() => caches.match('/article-offline.include')),
        // The bottom of the page
        caches.match('/article-bottom.include')
      ];

      // Merge them all together.
      const {done, response} = await mergeResponses(parts);

      // Wait until the stream is complete.
      event.waitUntil(done);

      // Return the merged response.
      return response;
    }());
  }
});

Dans l'exemple ci-dessus, mergeResponses est une petite fonction qui fusionne les flux de chaque requête. Cela signifie que nous pouvons afficher l'en-tête mis en cache pendant que le contenu réseau est diffusé en streaming.

Cette méthode est plus rapide que le modèle "app shell", car la requête réseau est effectuée en même temps que la requête de page, et le contenu peut être diffusé sans astuces majeures.

Toutefois, la requête pour includeURL sera retardée par le temps de démarrage du service worker. Nous pouvons également utiliser le préchargement de navigation pour résoudre ce problème, mais dans ce cas, nous ne voulons pas précharger la page entière, mais plutôt une inclusion.

Pour ce faire, un en-tête est envoyé avec chaque demande de préchargement :

Service-Worker-Navigation-Preload: true

Le serveur peut l'utiliser pour envoyer un contenu différent pour les requêtes de préchargement de navigation que pour une requête de navigation normale. N'oubliez pas d'ajouter un en-tête Vary: Service-Worker-Navigation-Preload pour que les caches sachent que vos réponses sont différentes.

Nous pouvons maintenant utiliser la requête de préchargement :

// Try to use the preload
const networkContent = Promise.resolve(event.preloadResponse)
  // Else do a normal fetch
  .then(r => r || fetch(includeURL))
  // A fallback if the network fails.
  .catch(() => caches.match('/article-offline.include'));

const parts = [
  caches.match('/article-top.include'),
  networkContent,
  caches.match('/article-bottom')
];

Modifier l'en-tête

Par défaut, la valeur de l'en-tête Service-Worker-Navigation-Preload est true, mais vous pouvez la définir sur la valeur de votre choix :

navigator.serviceWorker.ready.then(registration => {
  return registration.navigationPreload.setHeaderValue(newValue);
}).then(() => {
  console.log('Done!');
});

Par exemple, vous pouvez le définir sur l'ID du dernier post que vous avez mis en cache localement, afin que le serveur ne renvoie que les données les plus récentes.

Obtenir l'état

Vous pouvez rechercher l'état de la préchargement de navigation à l'aide de getState :

navigator.serviceWorker.ready.then(registration => {
  return registration.navigationPreload.getState();
}).then(state => {
  console.log(state.enabled); // boolean
  console.log(state.headerValue); // string
});

Merci beaucoup à Matt Falkenhagen et Tsuyoshi Horo pour leur travail sur cette fonctionnalité et pour leur aide dans la rédaction de cet article. Un grand merci à toutes les personnes impliquées dans l'effort de normalisation.