La carga previa de navegación te permite superar el tiempo de inicio del service worker realizando solicitudes en paralelo.
Resumen
- En algunas situaciones, el tiempo de inicio del trabajador de servicio puede retrasar una respuesta de red.
- La carga previa de navegación, disponible en los tres motores de navegador principales, corrige este problema, ya que te permite realizar la solicitud en paralelo con el inicio del trabajador de servicio.
- Puedes distinguir las solicitudes de carga previa de las navegaciones normales con un encabezado y publicar contenido diferente.
El problema
Cuando navegas a un sitio que usa un trabajador de servicio para controlar los eventos de recuperación, el navegador le solicita una respuesta al trabajador de servicio. Esto implica iniciar el service worker (si aún no se está ejecutando) y enviar el evento de recuperación.
El tiempo de inicio depende del dispositivo y de las condiciones. Por lo general, es de alrededor de 50 ms. En dispositivos móviles, es más bien de 250 ms. En casos extremos (dispositivos lentos, CPU en problemas), puede superar los 500 ms. Sin embargo, como el trabajador de servicio permanece activo durante un tiempo determinado por el navegador entre eventos, solo obtienes esta demora ocasionalmente, por ejemplo, cuando el usuario navega a tu sitio desde una pestaña nueva o desde otro sitio.
El tiempo de inicio no es un problema si respondes desde la caché, ya que el beneficio de omitir la red es mayor que la demora del inicio. Sin embargo, si respondes a través de la red, haz lo siguiente:
El inicio del service worker retrasa la solicitud de red.
Seguimos reduciendo el tiempo de inicio mediante la caché de código en V8, la omisión de los service workers que no tienen un evento de recuperación, el inicio de service workers de forma especulativa y otras optimizaciones. Sin embargo, el tiempo de inicio siempre será mayor que cero.
Facebook nos informó sobre el impacto de este problema y solicitó una forma de realizar solicitudes de navegación en paralelo:
La carga previa de navegación al rescate
La carga previa de navegación es una función que te permite decir: "Cuando el usuario realice una solicitud de navegación GET, inicia la solicitud de red mientras se inicia el trabajador del servicio".
La demora del inicio sigue presente, pero no bloquea la solicitud de red, por lo que el usuario obtiene el contenido antes.
Este es un video de cómo funciona, en el que se le asigna al trabajador de servicio una demora de inicio deliberada de 500 ms con un bucle while:
Esta es la demostración. Para obtener los beneficios de la precarga de navegación, necesitarás un navegador que la admita.
Cómo activar la carga previa de navegación
addEventListener('activate', event => {
event.waitUntil(async function() {
// Feature-detect
if (self.registration.navigationPreload) {
// Enable navigation preloads!
await self.registration.navigationPreload.enable();
}
}());
});
Puedes llamar a navigationPreload.enable()
cuando quieras o inhabilitarlo con navigationPreload.disable()
. Sin embargo, como tu evento fetch
debe usarlo, es mejor habilitarlo y, luego, inhabilitarlo en el evento activate
de tu trabajador de servicio.
Cómo usar la respuesta precargada
Ahora, el navegador realizará cargas previas para las navegaciones, pero aún debes usar la respuesta:
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
es una promesa que se resuelve con una respuesta en los siguientes casos:
- La precarga de navegación está habilitada.
- La solicitud es una solicitud
GET
. - La solicitud es una solicitud de navegación (que los navegadores generan cuando cargan páginas, incluidos los iframes).
De lo contrario, event.preloadResponse
sigue allí, pero se resuelve con undefined
.
Respuestas personalizadas para cargas previas
Si tu página necesita datos de la red, la forma más rápida es solicitarlos en el servicio de trabajo y crear una sola respuesta transmitida que contenga partes de la caché y partes de la red.
Supongamos que queremos mostrar un artículo:
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;
}());
}
});
En el ejemplo anterior, mergeResponses
es una pequeña función que combina los flujos de cada solicitud. Esto significa que podemos mostrar el encabezado almacenado en caché mientras se transmite el contenido de la red.
Esto es más rápido que el modelo de "shell de la app", ya que la solicitud de red se realiza junto con la solicitud de página, y el contenido se puede transmitir sin hacks importantes.
Sin embargo, la solicitud de includeURL
se retrasará debido al tiempo de inicio del service worker. También podemos usar la carga previa de navegación para corregir esto, pero en este caso no queremos cargar previamente la página completa, sino una inclusión.
Para admitir esto, se envía un encabezado con cada solicitud de carga previa:
Service-Worker-Navigation-Preload: true
El servidor puede usar esto para enviar contenido diferente para las solicitudes de carga previa de navegación que para una solicitud de navegación normal. Solo recuerda agregar un encabezado Vary: Service-Worker-Navigation-Preload
para que las cachés sepan que tus respuestas difieren.
Ahora podemos usar la solicitud de carga previa:
// 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')
];
Cambia el encabezado
De forma predeterminada, el valor del encabezado Service-Worker-Navigation-Preload
es true
, pero puedes establecerlo en lo que quieras:
navigator.serviceWorker.ready.then(registration => {
return registration.navigationPreload.setHeaderValue(newValue);
}).then(() => {
console.log('Done!');
});
Por ejemplo, puedes establecerlo en el ID de la última publicación que almacenaste en caché de forma local, de modo que el servidor solo muestre datos más recientes.
Cómo obtener el estado
Puedes buscar el estado de la precarga de navegación con getState
:
navigator.serviceWorker.ready.then(registration => {
return registration.navigationPreload.getState();
}).then(state => {
console.log(state.enabled); // boolean
console.log(state.headerValue); // string
});
Muchas gracias a Matt Falkenhagen y Tsuyoshi Horo por su trabajo en esta función y su ayuda con este artículo. Y un gran agradecimiento a todas las personas involucradas en el esfuerzo de estandarización