Acelera el service worker con cargas previas de navegación

La precarga de navegación te permite superar el tiempo de inicio del service worker realizando solicitudes en paralelo.

Navegadores compatibles

  • Chrome: 59.
  • Límite: 18.
  • Firefox: 99.
  • Safari: 15.4.

Origen

Resumen

El problema

Cuando navegas a un sitio que usa un service worker para controlar los eventos de recuperación, el navegador le pide una respuesta al service worker. Esto implica iniciar el service worker (si aún no se está ejecutando) y despachar el evento de recuperación.

El tiempo de inicio depende del dispositivo y de las condiciones. Por lo general, es de unos 50 ms. En dispositivos móviles, es más de 250 ms. En casos extremos (dispositivos lentos, CPU en peligro), puede tardar más de 500 ms. Sin embargo, dado que el service worker permanece activo entre eventos durante un tiempo determinado por el navegador, solo recibes esta demora ocasionalmente; por ejemplo, cuando el usuario navega hacia 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 el retraso del inicio. Pero si respondes usando la red...

Inicio del SW
Solicitud de navegación

El inicio del service worker retrasa la solicitud de red.

Para seguir reduciendo el tiempo de inicio, usamos el almacenamiento en caché de código en V8, omitimos service workers que no tienen un evento de recuperación, lanzamos 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:

Inicio del SW
Solicitud de navegación

Carga previa de Navigation al rescate

La precarga de navegación es una función que te permite decir: "Cuando el usuario hace una solicitud de navegación GET, se inicia la solicitud de red mientras se inicia el service worker".

La demora en el inicio sigue presente, pero no bloquea la solicitud de red, por lo que el usuario obtiene contenido antes.

Este es un video de cómo funciona, donde el service worker recibe un retraso de inicio intencional de 500 ms mediante un bucle while:

Esta es la demostración en sí. Para obtener los beneficios de la precarga de navegación, necesitarás un navegador compatible.

Activar la precarga 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 necesita usarse el evento fetch, es mejor habilitarlo o inhabilitarlo en el evento activate del service worker.

Cómo usar la respuesta precargada

Ahora, el navegador realizará precargas 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 GET.
  • La solicitud es una solicitud de navegación (que los navegadores generan cuando cargan páginas, incluidos iframes).

De lo contrario, event.preloadResponse sigue allí, pero se resuelve con undefined.

Si tu página necesita datos de la red, la forma más rápida es solicitarlos al service worker y crear una única respuesta de transmisión que contenga partes de la caché y otras 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 las transmisiones 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 "shell de la app" ya que se realiza la solicitud de red junto con la solicitud de la página, y el contenido puede transmitirse sin hackeos graves.

Sin embargo, el tiempo de inicio del service worker retrasará la solicitud de includeURL. También podemos usar la precarga de navegación para corregir esto, pero, en este caso, no queremos precargar la página completa, sino precargar una inclusión.

Para ello, se envía un encabezado con cada solicitud de precarga:

Service-Worker-Navigation-Preload: true

El servidor puede usar esto para enviar contenido diferente para las solicitudes de precarga de navegación al que enviaría 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 precarga:

// 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')
];

Cambiar el encabezado

De forma predeterminada, el valor del encabezado Service-Worker-Navigation-Preload es true, pero puedes configurarlo como 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 entrada que hayas almacenado en caché localmente, de modo que el servidor solo muestre datos más recientes.

Obtén 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 por ayudarme con este artículo. Queremos agradecerles a todos los que participan en el esfuerzo de estandarización