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

Le préchargement de navigation vous permet de contourner le temps de démarrage d'un service worker en effectuant des requêtes en parallèle.

Jake Archibald
Jake Archibald

Navigateurs pris en charge

  • Chrome: 59 <ph type="x-smartling-placeholder">
  • Edge: 18 <ph type="x-smartling-placeholder">
  • Firefox: 99 <ph type="x-smartling-placeholder">
  • Safari: 15.4. <ph type="x-smartling-placeholder">

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 au service worker une réponse. Cela implique de démarrer le service worker (s'il n'est pas déjà en cours d'exécution) et de déclencher 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 atteint 250 ms. Dans les cas extrêmes (appareils lents, processeur en détresse), elle peut dépasser 500 ms. Toutefois, étant donné que le service worker reste actif pendant un laps de temps déterminé par le navigateur entre les événements, ce délai n'est constaté qu'occasionnellement, par exemple lorsque l'utilisateur accède à votre site à partir d'un nouvel onglet ou d'un autre site.

Le temps de démarrage ne pose pas de problème si vous répondez à partir du cache, car l'avantage d'ignorer le réseau est supérieur au délai de démarrage. Mais si vous répondez via le réseau...

Démarrage logiciel
Requête de navigation

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

Nous continuons à 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 de récupération, en lançant les service workers de manière spéculative et en effectuant d'autres optimisations. Cependant, le temps de démarrage est toujours supérieur à zéro.

Facebook a porté à notre attention l'impact de ce problème et a demandé un moyen d'exécuter les requêtes de navigation en parallèle:

Démarrage logiciel
Requête de navigation

Le préchargement de la navigation à la rescousse

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

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

Voici une vidéo illustrant cette fonctionnalité, dans laquelle le service worker reçoit un délai de démarrage délibéré de 500 ms à l'aide d'une boucle de type "while-loop" :

Voici la démonstration. Pour profiter des avantages du préchargement de la 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() à tout moment 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 effectuera maintenant des préchargements pour les navigations, mais vous devrez toujours utiliser la réponse suivante:

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é.
  • Il s'agit d'une requête GET.
  • Il s'agit d'une requête de navigation (que les navigateurs génèrent lors du chargement de 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 issues du réseau, le moyen le plus rapide consiste à les demander dans le service worker et à créer une réponse diffusée unique contenant certaines parties du cache et des parties provenant du réseau.

Imaginons que nous voulions 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 ce qui précède, 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 lorsque le contenu du réseau est en flux continu.

Cette méthode est plus rapide que le "shell de l'application". au fur et à mesure que la requête réseau est effectuée en même temps que la demande de page. De plus, le contenu peut être diffusé sans piratage majeur.

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

Pour ce faire, un en-tête est envoyé avec chaque requête 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 par rapport à une requête de navigation standard. N'oubliez pas d'ajouter un en-tête Vary: Service-Worker-Navigation-Preload afin que les caches sachent que vos réponses diffèrent.

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 comme vous le souhaitez:

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

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

Obtenir l'état

Vous pouvez rechercher l'état du préchargement de la 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
});

Un grand merci à Matt Falkenhagen et Tsuyoshi Horo pour leur travail sur cette fonctionnalité et pour leur aide concernant cet article. Nous remercions sincèrement toutes les personnes impliquées dans l'effort de normalisation.