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

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

Jake Archibal
Jake Archibal

Navigateurs pris en charge

  • 59
  • 18
  • 99
  • 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 déclencher l'événement "fetch".

Le temps de démarrage dépend de l'appareil et des conditions. Elle est généralement d'environ 50 ms. Sur mobile, cela ressemble plus à 250 ms. Dans les cas extrêmes (appareils lents, processeur en détresse), la vitesse peut être supérieure à 500 ms. Toutefois, comme le service worker reste éveillé pendant un laps de temps déterminé par le navigateur entre les événements, ce délai ne s'applique 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 de sauter le réseau est plus important que le délai de démarrage. En revanche, si vous répondez via le réseau...

Démarrage logiciel
Requête de navigation

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

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 de récupération, en lançant des 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 attiré notre attention sur l'impact de ce problème et nous a demandé un moyen d'exécuter des requêtes de navigation en parallèle:

Démarrage logiciel
Requête de navigation



Nous nous sommes dit : "Oui, ça semble juste".

À la rescousse : "Navigation préchargement"

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

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

Dans cette vidéo, dans laquelle le service worker reçoit un délai de démarrage délibéré de 500 ms à l'aide d'une boucle "when" :

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(). Cependant, comme votre événement fetch doit l'utiliser, il est préférable de l'activer ou de le désactiver dans l'événement activate de votre service worker.

Utiliser la réponse préchargée

Le navigateur va maintenant effectuer 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 la navigation est activé.
  • Il s'agit d'une requête GET.
  • Il s'agit d'une requête de navigation (générée par les navigateurs lorsqu'ils chargent des pages, y compris des iFrames).

Sinon, event.preloadResponse est toujours présent, mais se résout avec undefined.

Si votre page a besoin de données provenant du réseau, le moyen le plus rapide consiste à les demander au service worker et à créer une réponse unique diffusée en continu, contenant des parties du cache et des parties 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 pendant que le contenu du réseau diffuse du contenu.

Ce modèle est plus rapide que le modèle "App shell", car la requête réseau est effectuée avec la demande de page, et le contenu peut être diffusé sans piratage majeur.

Cependant, la requête pour includeURL sera retardée en raison du 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, nous voulons 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 et pour une requête de navigation standard. N'oubliez pas d'ajouter un en-tête Vary: Service-Worker-Navigation-Preload pour 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, définir cet ID sur l'ID du dernier message 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 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 nous vous remercions pour cet article. Un grand merci à toutes les personnes impliquées dans l'effort de normalisation

Fait partie de la série sur les nouvelles technologies interopérables