Le préchargement de navigation vous permet de réduire le temps de démarrage du service worker en envoyant des requêtes en parallèle.
Résumé
- Dans certains cas, le temps de démarrage du service worker peut retarder une réponse réseau.
- Disponible dans les trois principaux moteurs de navigateur, le préchargement de navigation résout ce problème en vous permettant d'effectuer la requête en parallèle du démarrage du service worker.
- Vous pouvez distinguer les requêtes de préchargement des navigations régulières à l'aide d'un en-tête et diffuser du contenu différent.
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. Il 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é), ce délai 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, ce délai ne se produit que de temps en temps, 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 sauter le réseau est supérieur au délai de démarrage. Toutefois, si vous répondez via le réseau :
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 services workers qui ne disposent pas d'événement de récupération, en lançant des services 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 nous a signalé l'impact de ce problème et nous a demandé un moyen d'effectuer des requêtes de navigation en parallèle:
La précharge de navigation à la rescousse
Le préchargement de navigation est une fonctionnalité qui vous permet de spécifier : "Lorsque l'utilisateur envoie 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 rapidement.
Voici une vidéo de son fonctionnement, où le délai de démarrage du service worker est délibérément défini sur 500 ms à l'aide d'une boucle while:
Voici la démonstration elle-même. Pour profiter 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()
à tout moment ou le désactiver avec navigationPreload.disable()
. Toutefois, comme votre événement fetch
doit s'en servir, 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 la navigation est activé.
- La requête est une requête
GET
. - La requête est 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 il est résolu avec undefined
.
Réponses personnalisées pour les préchargements
Si votre page a besoin de données du réseau, le moyen le plus rapide est de les demander dans le service worker et de créer une seule réponse en streaming contenant des parties du cache et 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 du réseau est diffusé.
Cette approche est plus rapide que le modèle "shell d'application", car la requête réseau est envoyée avec la requête de page, et le contenu peut être diffusé en streaming sans gros piratages.
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 complète, mais 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 utiliser cette information pour envoyer un contenu différent pour les requêtes de préchargement de navigation que 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 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 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 post 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 consulter 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
});
Merci beaucoup à Matt Falkenhagen et Tsuyoshi Horo pour leur travail sur cette fonctionnalité et leur aide pour cet article. Merci à tous ceux qui ont participé à l'effort de normalisation.