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.
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, la 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 différents contenus.
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…
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 :
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
.
Réponses personnalisées pour les préchargements
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.