Estrategias para el almacenamiento en caché de service worker

Hasta ahora, solo había menciones y pequeños fragmentos de código de la interfaz Cache. Para usar service workers de manera eficaz, es necesario adoptar una o más estrategias de almacenamiento en caché, lo que requiere un poco de familiaridad con la interfaz Cache.

Una estrategia de almacenamiento en caché es una interacción entre el evento fetch de un service worker y la interfaz Cache. La forma en que se escribe una estrategia de almacenamiento en caché depende; por ejemplo, puede ser preferible manejar las solicitudes de elementos estáticos de manera diferente a los documentos, y esto afecta la forma en que se compone una estrategia de almacenamiento en caché.

Antes de pasar a las estrategias en sí, tomémonos un segundo para hablar de lo que no es la interfaz Cache, qué es y un resumen rápido de algunos de los métodos que ofrece para administrar las memorias caché de los service worker.

La interfaz Cache en comparación con la caché HTTP

Si nunca trabajaste con la interfaz Cache, puede ser tentador pensar que es lo mismo que la caché HTTP o, al menos, que está relacionado con ella. Este no es el caso.

  • La interfaz Cache es un mecanismo de almacenamiento en caché completamente independiente de la caché HTTP.
  • Cualquier configuración de Cache-Control que uses para influir en la caché HTTP no influye en los elementos que se almacenan en la interfaz Cache.

Resulta útil pensar en las cachés del navegador como en capas. La caché HTTP es una caché de bajo nivel controlada por pares clave-valor con directivas expresadas en encabezados HTTP.

Por el contrario, la interfaz Cache es una caché de alto nivel controlada por una API de JavaScript. Esto ofrece más flexibilidad que cuando se usan pares clave-valor HTTP relativamente simples y es la mitad de lo que hace posibles las estrategias de almacenamiento en caché. Estos son algunos métodos de API importantes para las cachés de service worker:

  • CacheStorage.open para crear una nueva instancia de Cache.
  • Cache.add y Cache.put para almacenar respuestas de red en la caché de un service worker.
  • Cache.match para ubicar una respuesta almacenada en caché en una instancia de Cache.
  • Cache.delete para quitar una respuesta almacenada en caché de una instancia de Cache.

Esos son solo algunos. Existen otros métodos útiles, pero estos son los básicos que verás más adelante en esta guía.

El modesto evento fetch

La otra mitad de una estrategia de almacenamiento en caché es el evento fetch del service worker. Hasta ahora, en esta documentación, escuchaste un poco sobre la “intercepción de solicitudes de red”, y el evento fetch dentro de un service worker es donde esto sucede:

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('install', (event) => {
  event.waitUntil(caches.open(cacheName));
});

self.addEventListener('fetch', async (event) => {
  // Is this a request for an image?
  if (event.request.destination === 'image') {
    // Open the cache
    event.respondWith(caches.open(cacheName).then((cache) => {
      // Respond with the image from the cache or from the network
      return cache.match(event.request).then((cachedResponse) => {
        return cachedResponse || fetch(event.request.url).then((fetchedResponse) => {
          // Add the network response to the cache for future visits.
          // Note: we need to make a copy of the response to save it in
          // the cache and use the original as the request response.
          cache.put(event.request, fetchedResponse.clone());

          // Return the network response
          return fetchedResponse;
        });
      });
    }));
  } else {
    return;
  }
});

Este es un ejemplo sencillo, que puedes ver en acción, pero que ofrece una idea de lo que pueden hacer los service workers. El código anterior hace lo siguiente:

  1. Inspecciona la propiedad destination de la solicitud para ver si se trata de una solicitud de imagen.
  2. Si la imagen está en la caché del service worker, puedes entregarla desde allí. De lo contrario, recupera la imagen de la red, almacena la respuesta en la caché y muestra la respuesta de la red.
  3. Todas las demás solicitudes se pasan a través del service worker sin interacción con la caché.

El objeto event de una recuperación contiene una propiedad request con fragmentos de información útiles que te ayudarán a identificar el tipo de cada solicitud:

  • url, que es la URL de la solicitud de red que controla el evento fetch en este momento
  • method, que es el método de solicitud (p.ej., GET o POST).
  • mode, que describe el modo de la solicitud Por lo general, se usa un valor de 'navigate' para distinguir las solicitudes de documentos HTML de otras solicitudes.
  • destination, que describe el tipo de contenido que se solicita de una manera que evita el uso de la extensión de archivo del elemento solicitado.

Una vez más, asincronía es el nombre del juego. Recuerda que el evento install ofrece un método event.waitUntil que toma una promesa y espera a que se resuelva antes de continuar con la activación. El evento fetch ofrece un método event.respondWith similar que puedes usar para mostrar el resultado de una solicitud fetch asíncrona o una respuesta que muestra el método match de la interfaz Cache.

Estrategias de almacenamiento en caché

Ahora que estás familiarizado con las instancias de Cache y el controlador de eventos fetch, estás listo para profundizar en algunas estrategias de almacenamiento en caché del service worker. Si bien las posibilidades son prácticamente ilimitadas, en esta guía, te enfocaremos en las estrategias que se incluyen en Workbox para que puedas tener una idea de lo que sucede en su funcionamiento.

Solo caché

Muestra el flujo de la página al service worker y a la caché.

Comencemos con una estrategia de almacenamiento en caché simple que llamaremos "Solo caché". Es decir, cuando el service worker tenga el control de la página, las solicitudes coincidentes solo irán a la caché. Esto significa que cualquier elemento almacenado en caché deberá almacenarse previamente en caché para estar disponible para que el patrón funcione, y esos elementos nunca se actualizarán en la caché hasta que se actualice el service worker.

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

// Assets to precache
const precachedAssets = [
  '/possum1.jpg',
  '/possum2.jpg',
  '/possum3.jpg',
  '/possum4.jpg'
];

self.addEventListener('install', (event) => {
  // Precache assets on install
  event.waitUntil(caches.open(cacheName).then((cache) => {
    return cache.addAll(precachedAssets);
  }));
});

self.addEventListener('fetch', (event) => {
  // Is this one of our precached assets?
  const url = new URL(event.request.url);
  const isPrecachedRequest = precachedAssets.includes(url.pathname);

  if (isPrecachedRequest) {
    // Grab the precached asset from the cache
    event.respondWith(caches.open(cacheName).then((cache) => {
      return cache.match(event.request.url);
    }));
  } else {
    // Go to the network
    return;
  }
});

Arriba, se muestra un array de elementos que se almacena en caché previamente en el momento de la instalación. Cuando el service worker controla las recuperaciones, comprobamos si la URL de solicitud que controla el evento fetch está en el array de los elementos almacenados en caché previamente. Si es así, tomamos el recurso de la caché y omitimos la red. Otras solicitudes pasan a través de la red, y solo de esta. Para ver esta estrategia en acción, consulta esta demostración con tu consola abierta.

Solo de red

Muestra el flujo de la página al service worker y a la red.

Lo contrario de “Solo caché” es “Solo red”, en el que una solicitud pasa a través de un service worker a la red, sin ninguna interacción con su caché. Esta es una buena estrategia para garantizar la actualización del contenido (piensa en el lenguaje de marcado), pero la desventaja es que nunca funcionará cuando el usuario no tenga conexión.

Garantizar que una solicitud pase a la red solo significa que no se llama a event.respondWith para una solicitud coincidente. Si deseas ser explícito, puedes colocar un return; vacío en la devolución de llamada de evento fetch para las solicitudes que quieras pasar a la red. Esto es lo que sucede en la demostración de la estrategia “Solo caché” para las solicitudes que no se almacenan previamente en la caché.

Caché primero, y recurrir a la red

Muestra el flujo desde la página hasta el service worker, hasta la caché y, luego, hacia la red si no está en la caché.

En esta estrategia, las cosas se vuelven un poco más involucradas. Para las solicitudes coincidentes, el proceso es el siguiente:

  1. La solicitud llega a la caché. Si el recurso está en la caché, publícalo desde allí.
  2. Si la solicitud no está en la caché, ve a la red.
  3. Una vez finalizada la solicitud de red, agrégala a la caché y, luego, muestra la respuesta de la red.

A continuación, verás un ejemplo de esta estrategia, que puedes probar en una demostración en vivo:

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('fetch', (event) => {
  // Check if this is a request for an image
  if (event.request.destination === 'image') {
    event.respondWith(caches.open(cacheName).then((cache) => {
      // Go to the cache first
      return cache.match(event.request.url).then((cachedResponse) => {
        // Return a cached response if we have one
        if (cachedResponse) {
          return cachedResponse;
        }

        // Otherwise, hit the network
        return fetch(event.request).then((fetchedResponse) => {
          // Add the network response to the cache for later visits
          cache.put(event.request, fetchedResponse.clone());

          // Return the network response
          return fetchedResponse;
        });
      });
    }));
  } else {
    return;
  }
});

Aunque este ejemplo solo abarca imágenes, esta es una excelente estrategia para aplicar a todos los recursos estáticos (como CSS, JavaScript, imágenes y fuentes), especialmente a los que tienen versiones de hash. Ofrece un aumento de velocidad para los elementos inmutables evitando las verificaciones de actualización del contenido con el servidor que la caché HTTP puede iniciarse. Lo más importante es que los elementos almacenados en caché estarán disponibles sin conexión.

Primero de la red y recurrir a la caché

Muestra el flujo de la página al service worker, a la red y, luego, al almacenamiento en caché si la red no está disponible.

Si quisieras girar la opción “Caché primero, luego la red en segundo lugar” y terminarás con la estrategia “Red primero, caché en segundo lugar”, que es como suena:

  1. Primero, vas a la red para obtener una solicitud y colocas la respuesta en la caché.
  2. Si estás sin conexión más adelante, recurrirás a la última versión de esa respuesta en la caché.

Esta estrategia es excelente para solicitudes HTML o a la API cuando, mientras estás en línea, deseas la versión más reciente de un recurso, pero deseas otorgar acceso sin conexión a la versión disponible más reciente. A continuación, se muestra cómo podría verse cuando se aplique a las solicitudes de HTML:

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('fetch', (event) => {
  // Check if this is a navigation request
  if (event.request.mode === 'navigate') {
    // Open the cache
    event.respondWith(caches.open(cacheName).then((cache) => {
      // Go to the network first
      return fetch(event.request.url).then((fetchedResponse) => {
        cache.put(event.request, fetchedResponse.clone());

        return fetchedResponse;
      }).catch(() => {
        // If the network is unavailable, get
        return cache.match(event.request.url);
      });
    }));
  } else {
    return;
  }
});

Puedes probar esto en una demostración. Primero, ve a la página. Es posible que debas volver a cargar la página para que la respuesta HTML se coloque en la caché. Luego, en las herramientas para desarrolladores, simula una conexión sin conexión y vuelve a cargar la página. La última versión disponible se entregará instantáneamente desde la caché.

En situaciones en las que la capacidad sin conexión es importante, pero necesitas equilibrar esa capacidad con el acceso a la versión más reciente de un poco de lenguaje de marcado o datos de API, la estrategia "Red primero, caché en segundo lugar" es una estrategia sólida que logra ese objetivo.

Revalidación inactiva durante el período de inactividad

Muestra el flujo de la página al service worker, a la caché y, luego, de la red a la caché.

De las estrategias que tratamos hasta ahora, la etapa “Stale-while-revalidate” es la más compleja. Es similar a las dos últimas estrategias en algunos aspectos, pero el procedimiento prioriza la velocidad de acceso de un recurso y, al mismo tiempo, lo mantiene actualizado en segundo plano. Esta estrategia es como la siguiente:

  1. En la primera solicitud de un elemento, recupéralo de la red, colócalo en la caché y muestra la respuesta de la red.
  2. En solicitudes posteriores, entrega el recurso desde la caché primero y, luego, “en segundo plano”, vuelve a solicitarlo en la red y actualiza la entrada de caché del elemento.
  3. Para las solicitudes posteriores, recibirás la última versión recuperada de la red que se colocó en la caché en el paso anterior.

Esta es una estrategia excelente para cosas que son otro importantes para mantenerse actualizados, pero no son cruciales. Piensa en cosas como avatares para un sitio de redes sociales. Se actualizan cuando los usuarios lo hacen, pero la versión más reciente no es estrictamente necesaria en cada solicitud.

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('fetch', (event) => {
  if (event.request.destination === 'image') {
    event.respondWith(caches.open(cacheName).then((cache) => {
      return cache.match(event.request).then((cachedResponse) => {
        const fetchedResponse = fetch(event.request).then((networkResponse) => {
          cache.put(event.request, networkResponse.clone());

          return networkResponse;
        });

        return cachedResponse || fetchedResponse;
      });
    }));
  } else {
    return;
  }
});

Puedes ver esto en acción en otra demostración en vivo, sobre todo si prestas atención a la pestaña Red en las herramientas para desarrolladores de tu navegador y a su visor CacheStorage (si las herramientas para desarrolladores de tu navegador tienen esa herramienta).

Pasemos a Workbox

En este documento, se concluye nuestra revisión de la API de service worker, así como de las APIs relacionadas, lo que significa que aprendiste lo suficiente sobre cómo usar los service workers de forma directa para comenzar a experimentar con Workbox.