Solicitudes con intención: Estrategias de Caché en la era de las PWAs

Mariana Ramírez
A List Apart en Español
15 min readJan 21, 2020

Por Aaron Gustafson. Original en inglés, traducción al español por Mariana Ramírez

En una época, dependíamos de los navegadores para manejar el caché por nosotros; como desarrolladores en esos días, teníamos muy poco control. Pero después vinieron las Aplicaciones Web Progresivas (PWAs), los Service Workers, y la API de Caché — y de repente tenemos un poder expansivo sobre lo que se pone en el caché y cómo se pone allí. Ahora podemos almacenar en caché todo lo que queramos… y ahí reside un problema potencial.

Los archivos multimedia — especialmente las imágenes — constituyen la mayor parte del peso promedio de las páginas en la actualidad, y está empeorando. Para mejorar el rendimiento, es tentador almacenar en caché tanto contenido como sea posible, pero ¿deberíamos hacerlo? En la mayoría de los casos, no. Incluso con toda esta nueva tecnología a nuestro alcance, el gran rendimiento sigue dependiendo de una regla simple: pide sólo lo que necesitas y haz cada petición lo más pequeña posible.

Para proporcionar la mejor experiencia posible para nuestros usuarios sin abusar de su conexión de red o de su disco duro, es hora de dar un giro a algunas de las mejores prácticas clásicas, experimentar con estrategias de almacenamiento en caché de multimedia y jugar un poco con algunos trucos de la API de caché que los Service Workers tienen escondidos bajo la manga.

Mejores intenciones

Todas esas lecciones que aprendimos optimizando páginas web para acceso telefónico se volvieron súper útiles de nuevo cuando el móvil despegó, y continúan siendo aplicables en el trabajo que hacemos para una audiencia global hoy en día. Las conexiones de red poco fiables o de alta latencia siguen siendo la norma en muchas partes del mundo, recordándonos que nunca es seguro asumir que una línea de base técnica se eleva de manera uniforme o en sincronía con su correspondiente vanguardia. Y eso es lo que sucede con las mejores prácticas de rendimiento: la historia ha confirmado que los enfoques que son buenos para el rendimiento ahora continuarán siendo buenos para el rendimiento en el futuro.

Antes de la llegada de los Service Workers, podíamos proporcionar algunas instrucciones a los navegadores con respecto a cuánto tiempo debían almacenar en caché un recurso en particular, pero eso era todo. Los documentos y activos descargados a la máquina de un usuario se dejarían en un directorio de su disco duro. Cuando el navegador preparaba una solicitud para un documento o activo en particular, primero miraba en el caché para ver si ya tenía lo que necesitaba y así evitar cargar la red.

Tenemos mucho más control sobre las peticiones de la red y el caché hoy en día, pero eso no nos exime de ser cuidadosos con los recursos de nuestras páginas web.

Solicita sólo lo que necesitas

Como mencioné, la web hoy es pésima con los archivos multimedia. Las imágenes y los videos se han convertido en medios dominantes. Pueden convertir bien cuando se trata de ventas y mercadeo, pero apenas tienen rendimiento cuando se trata de la velocidad de descarga y renderizado. Con esto en mente, todas y cada una de las imágenes (y videos, etc.) deberían tener que luchar por su lugar en la página.

Unos años atrás, una de mis recetas fue incluida en un artículo de un periódico sobre cocina con espirituosas (bebidas, no fantasmas). No estoy suscrito a la versión impresa de ese periódico, así que cuando salió el artículo fui al sitio para ver cómo había quedado. Durante un reciente rediseño, el sitio había decidido cargar todos los artículos en una caja de visualización modal casi a pantalla completa, en la parte superior de su página de inicio. Esto significaba que al solicitar el artículo se requerían las solicitudes de todos los activos asociados con la página del artículo más todos los contenidos y activos de la página de inicio. Ah, y la página de inicio tenía anuncios de video — en plural. Y sí, se reproducían automáticamente.

Abrí DevTools y descubrí que la página había sobrepasado los 15 MB de peso. Tim Kadlec había lanzado recientemente What Does My Site Cost?, así que decidí comprobar los daños. Resulta que el costo real de visualizar esa página para el usuario promedio en los EE.UU. era mayor que el costo de la versión impresa del periódico de ese día. Eso es un desastre.

Claro, podría culpar a la gente que construyó el sitio por brindarle un mal servicio a sus lectores, pero la realidad es que ninguno de nosotros va a trabajar con el objetivo de empeorar las experiencias de nuestros usuarios. Esto podría pasarle a cualquiera de nosotros. Podríamos pasar días escudriñando el funcionamiento de una página sólo para que algún comité decida poner encima de esa página cuidadosamente elaborada anuncios de video de reproducción automática tipo Times Square. ¡Imagínate cuánto peor sería si apiláramos dos páginas de rendimiento abismal una encima de la otra!

Los archivos multimedia pueden ser geniales para llamar la atención cuando la competencia es alta (por ejemplo, en la página de inicio de un periódico), pero cuando quieres que los lectores se enfoquen en una única tarea (por ejemplo, leer el artículo), su valor puede caer de importante a “lindo tenerlo”. Sí, los estudios han mostrado que las imágenes sobresalen al llamar la atención, pero una vez que el visitante está en la página del artículo, a nadie le importa; sólo estamos haciendo que la descarga sea más lenta y el acceso más costoso. La situación sólo empeora a medida que introducimos más archivos multimedia en la página.

Debemos hacer todo lo que esté en nuestras manos para reducir el peso de nuestras páginas, y así evitar las solicitudes de cosas que no añaden valor. Para empezar, si estás escribiendo un artículo acerca de una violación de datos, resiste el impulso de incluir esa foto ridícula de archivo de un tipo cualquiera con capucha escribiendo en un computador en una habitación muy oscura.

Solicita el archivo más pequeño que puedas

Ahora que hemos hecho un balance de lo que necesitamos incluir, debemos hacernos una pregunta crítica: ¿Cómo podemos entregarlo de la manera más rápida posible? Esto puede ser tan sencillo como elegir el formato de imagen más apropiado para el contenido presentado (y optimizarla) o tan complejo como recrear los activos por completo (por ejemplo, si el cambio de imágenes rasterizadas a imágenes vectoriales fuera más eficiente).

Ofrece formatos alternativos

Cuando se trata de formatos de imagen, ya no tenemos que elegir entre rendimiento y alcance. Podemos proporcionar múltiples opciones y dejar que el navegador decida cuál utilizar, en función de lo que pueda manejar.

Puedes lograr esto ofreciendo múltiples fuentes dentro de un elemento de imagen o video. Empieza por crear múltiples formatos de activos multimedia. Por ejemplo, con WebP y JPG, es probable que el WebP tenga un tamaño de archivo más pequeño que el JPG (pero verifica que sea así). Con estas fuentes alternativas, puedes soltarlas en una imagen así:

Los navegadores que reconocen el elemento de imagen comprobarán el elemento de origen antes de tomar una decisión acerca de cuál imagen solicitar. Si el navegador soporta el tipo MIME “image/webp,” iniciará una solicitud para la imagen en formato WebP. Si no lo hace (o si el navegador no reconoce la imagen), solicitará el JPG.

Lo bueno de este enfoque es que estás sirviendo la imagen más pequeña posible al usuario sin tener que recurrir a ningún tipo de JavaScript.

Puedes adoptar el mismo enfoque con los archivos de video:

Los navegadores que soportan WebM solicitarán la primera fuente, mientras que los navegadores que no lo hacen — pero entienden los videos MP4 — solicitarán la segunda. Los navegadores que no soportan el elemento de video volverán al párrafo sobre la descarga del archivo.

El orden de tus elementos fuente es importante. Los navegadores elegirán la primera fuente usable, así que si especificas un formato alternativo optimizado después de uno más ampliamente compatible, es posible que el formato alternativo nunca sea elegido.

Dependiendo de tu situación, puedes considerar la posibilidad de evitar este enfoque basado en el markup y manejar las cosas desde el servidor. Por ejemplo, si un JPG está siendo solicitado y el navegador soporta WebP (lo cual se indica en la cabecera Accept), no hay nada que te impida responder con una versión WebP del recurso. De hecho, algunos servicios de CDN — Cloudinary, por ejemplo — vienen directamente con este tipo de funcionalidad.

Ofrece diferentes tamaños

Aparte de los formatos, es posible que quieras ofrecer tamaños de imagen alternativos optimizados para el tamaño actual de la ventana del navegador. Después de todo, no tiene sentido una imagen que es 3 o 4 veces más grande que la pantalla donde aparece; eso es simplemente desperdiciar ancho de banda. Aquí es donde vienen las imágenes responsive.

Aquí hay un ejemplo:

Hay muchas cosas sucediendo en este elemento img sobrecargado, así que lo voy a descomponer:

  • Este img ofrece tres opciones de tamaño para un JPG determinado: 256 px de ancho (small.jpg), 512 px de ancho (medium.jpg), y 1024 px de ancho (large.jpg). Estas opciones se proporcionan en el atributo srcset con sus correspondientes width descriptors.
  • El src define una fuente de imagen por defecto, la cual funciona como una alternativa para los navegadores que no soportan srcset. Tu elección para la imagen por defecto probablemente dependerá del contexto y patrones de uso generales. A menudo recomendaría que la imagen más pequeña sea la predeterminada, pero si la mayoría de tu tráfico viene de navegadores de escritorio más antiguos, es posible que desees optar por la imagen de tamaño mediano.
  • El atributo sizes es una pista de presentación que informa al navegador cómo será renderizada la imagen en diferentes escenarios (su tamaño extrínseco) una vez que se haya aplicado el CSS. Este ejemplo en particular dice que la imagen será del tamaño completo de la ventana (100vw) hasta que la ventana alcance 30em de ancho (min-width: 30em), momento en el cual la imagen tendrá 30em de ancho. Puedes hacer que el valor de los tamaños sea tan complicado o tan simple como desees; si lo omites, los navegadores usarán el valor predeterminado de 100vw.

Incluso puedes combinar este enfoque con formatos alternativos y recortes dentro de una misma imagen.

Todo esto es para decir que tienes varias herramientas a tu disposición para entregar archivos multimedia de carga rápida, ¡así que úsalas!

Aplaza las solicitudes (cuando sea posible)

Hace años, Internet Explorer 11 introdujo un nuevo atributo que permitía a los desarrolladores quitar la prioridad a elementos img específicos para acelerar el renderizado de la página: lazyload. Ese atributo nunca llegó a ninguna parte, en cuanto a estándares, pero fue un intento sólido de aplazar la carga de imágenes hasta que las imágenes estén a la vista (o cerca de ella) sin tener que involucrar JavaScript.

Desde entonces ha habido innumerables implementaciones basadas en JavaScript de carga diferida de imágenes, pero recientemente Google también se inclinó por un enfoque más declarativo, utilizando un atributo diferente: loading.

El atributo loading soporta tres valores (“auto,” “lazy,” y “eager”) para definir cómo se debe introducir un recurso. Para nuestros propósitos, el valor “lazy” es el más interesante porque difiere la descarga del recurso hasta que alcanza una distancia calculada desde la ventana de visualización.

Añadiendo eso a la mezcla…

Este atributo ofrece una pequeña mejora del rendimiento en los navegadores basados en Chromium. Esperemos que se convierta en un estándar y sea acogido por otros navegadores en el futuro, pero mientras tanto no hace ningún daño incluirlo porque los navegadores que no entienden el atributo simplemente lo ignoran.

Este enfoque complementa una estrategia de priorización de multimedia bastante bien, pero antes de que llegar a eso, quiero examinar más de cerca los Service Workers.

Manipula las solicitudes en un Service Worker

Los Service Workers son un tipo especial de Web Worker con la habilidad de interceptar, modificar y responder a todas las solicitudes de la red a través de la API Fetch. También tienen acceso a la API Cache, así como a otros almacenes de datos asíncronos del lado del cliente como IndexedDB para el almacenamiento de recursos.

Cuando se instala un Service Worker, puede engancharse a ese evento y preparar el caché con recursos que desees utilizar más adelante. Mucha gente aprovecha esta oportunidad para obtener copias de activos globales, incluyendo estilos, scripts, logos y similares, pero también puedes utilizarla para almacenar en caché imágenes para usarlas cuando fallan las solicitudes de red.

Mantén una imagen de reserva

Asumiendo que quieres utilizar una solución alternativa en más de una receta de red, puedes configurar una función con nombre que responderá con ese recurso:

Luego, dentro de un manejador de eventos fetch, puedes usar esta función para proveer esa imagen de respaldo cuando las solicitudes de imágenes fallen en la red:

Cuando la red está disponible, los usuarios obtienen el comportamiento esperado:

Los avatares de las redes sociales se renderizan como se espera cuando la red está disponible.

Pero cuando la red se interrumpe, las imágenes se intercambian automáticamente por un respaldo, y la experiencia de usuario sigue siendo aceptable:

Un avatar de respaldo genérico se muestra cuando la red no está disponible.

En la superficie, este enfoque puede no parecer muy útil en términos de rendimiento, ya que esencialmente has añadido una descarga de imagen adicional a la mezcla. Sin embargo, con este sistema, se abren algunas oportunidades bastante sorprendentes.

Respeta la elección del usuario de ahorrar datos

Algunos usuarios reducen su consumo de datos entrando en un modo “lite” o activando una función de “ahorro de datos”. Cuando esto sucede, los navegadores suelen enviar un encabezado Save-Data con sus solicitudes de red.

Dentro de tu Service Worker, puedes buscar este encabezado y ajustar tus respuestas de forma acorde. Primero, busca el encabezado:

Luego, con tu manejador fetch de imágenes, podrías elegir responder preventivamente con la imagen de respaldo en lugar de ir por la red:

Incluso puedes llevar esto un paso más allá y ajustar respondWithFallbackImage() para proporcionar imágenes alternativas basadas en la solicitud original. Para ello, definirías varias alternativas globalmente en el Service Worker:

Ambos archivos deben ser almacenados en caché durante el evento de instalación del Service Worker:

Por último, en respondWithFallbackImage() puedes servir la imagen apropiada basada en la URL que se obtiene. En mi sitio, los avatares se extraen desde Webmention.io, así que hago pruebas para eso.

Con este cambio, necesitaré actualizar el manejador fetch para que pase el request.url como un argumento para respondWithFallbackImage(). Una vez hecho esto, cuando la red se interrumpe termino viendo algo como esto:

Una mención web que contiene tanto un avatar como una imagen incrustada se renderizará con dos respaldos diferentes cuando el encabezado Save-Data esté presente.

A continuación, necesitamos establecer algunas directrices generales para manejar los activos multimedia — basadas en la situación, por supuesto.

La estrategia de almacenamiento en caché: priorizar ciertos medios

En mi experiencia, los archivos multimedia — especialmente las imágenes — en la web tienden a caer en tres categorías de necesidad. En un extremo del espectro están los elementos que no añaden un valor significativo. En el otro extremo del espectro están los activos críticos que sí añaden valor, como los diagramas y gráficos que son esenciales para entender el contenido que les rodea. En algún punto intermedio está lo que yo llamaría los archivos multimedia que es “lindo tener”. Añaden valor a la experiencia principal de una página pero no son fundamentales para comprender el contenido.

Si consideras tus archivos multimedia con esta división en mente, puedes establecer algunas pautas generales para manejar cada uno de ellos, según la situación. En otras palabras, una estrategia de almacenamiento en caché.

Estrategia de carga de archivos multimedia, desglosada según lo crítico que sea un activo para comprender una interfaz

Cuando se trata de eliminar la ambigüedad entre lo que es crítico y lo que es lindo tener, es útil tener esos recursos organizados en directorios separados (o similares). De esa forma podemos añadir alguna lógica al Service Worker que pueda ayudarle a decidir cuál es cuál. Por ejemplo, en mi sitio personal, las imágenes críticas son auto-alojadas o provienen del sitio web de mi libro. Sabiendo eso, puedo escribir expresiones regulares que coincidan con esos dominios:

Con esa variable high_priority definida, puedo crear una función que me permita saber si una solicitud de imagen determinada (por ejemplo) es una solicitud de alta prioridad o no:

Añadir soporte para priorizar solicitudes multimedia sólo requiere añadir un nuevo condicional en el manejador de eventos fetch, como lo hicimos con Save-Data. Tu receta específica para manejar la red y el caché seguramente variarán, pero aquí está cómo elegí mezclar esta lógica dentro de las solicitudes de imágenes:

Podemos aplicar este enfoque prioritario a muchos tipos de activos. Incluso podríamos usarlo para controlar qué páginas se sirven primero-desde-el-caché vs. primero-desde-la-red (cache-first vs. network-first).

Mantén el caché ordenado

La capacidad de controlar qué recursos se almacenan en el disco es una gran oportunidad, pero también conlleva una enorme responsabilidad no abusar de ella.

Es probable que cada estrategia de almacenamiento en caché difiera, al menos un poco. Si estamos publicando un libro en línea, por ejemplo, puede tener sentido almacenar en caché todos los artículos, imágenes, etc. para ser vistos fuera de línea. Hay una cantidad fija de contenido y — suponiendo que no hay un montón de imágenes y videos pesados — los usuarios se beneficiarán de no tener que descargar cada capítulo por separado.

En un sitio de noticias, sin embargo, almacenar en caché cada artículo y foto llenará rápidamente los discos duros de nuestros usuarios. Si un sitio ofrece una cantidad indeterminada de páginas y recursos, es crítico tener una estrategia de almacenamiento en caché que ponga límites estrictos a la cantidad de recursos que estamos almacenando en el disco.

Una forma de hacer esto es crear varios bloques diferentes asociados con el almacenamiento en caché de diferentes formas de contenido. Los cachés de contenido más efímero pueden tener límites estrictos en cuanto a la cantidad de elementos que pueden ser almacenados. Por supuesto, todavía estamos sujetos a los límites de almacenamiento del dispositivo, pero ¿realmente queremos que nuestro sitio ocupe hasta 2 GB del disco duro de alguien?

Aquí hay un ejemplo, nuevamente de mi propio sitio:

Aquí he definido varias cachés, cada una con un name usado para direccionarla en la API de Caché y un prefijo de version. La version es definida en otra parte del Service Worker, y me permite purgar todas las cachés a la vez si es necesario.

Con excepción de la caché static, que se utiliza para los activos estáticos, cada caché tiene un limit en el número de elementos que se pueden almacenar. Yo sólo almaceno en caché las 5 páginas más recientes que alguien ha visitado, por ejemplo. Las imágenes se limitan a las 75 más recientes, y así sucesivamente. Este es un enfoque que Jeremy Keith esboza en su fantástico libro Going Offline (que realmente deberías leer si todavía no lo has hecho — aquí tienes una muestra).

Con estas definiciones de caché, puedo limpiar mis cachés periódicamente y podar los elementos más antiguos. Aquí está el código recomendado por Jeremy para este enfoque:

Podemos hacer que este código se ejecute cada vez que se cargue una nueva página. Al ejecutarlo en el Service Worker, lo hace en un hilo separado y no arrastrará la capacidad de respuesta del sitio. Lo activamos publicando un mensaje (usando postMessage()) en el Service Worker desde el hilo principal de JavaScript:

El último paso en el cableado de todo esto es configurar el Service Worker para que reciba el mensaje:

Aquí, el Service Worker escucha los mensajes entrantes y responde a la petición de “limpieza” ejecutando trimCache() en cada uno de los cubos de caché con un limit definido.

Este enfoque no es de ninguna manera elegante, pero funciona. Sería mucho mejor tomar decisiones acerca de la purga de las respuestas almacenadas en caché en función de la frecuencia de acceso a cada elemento y/o del espacio que ocupa en el disco. (Eliminar los elementos en caché basándose únicamente en cuándo fueron almacenados no es tan útil). Lamentablemente, no tenemos ese nivel de detalle cuando se trata de inspeccionar cachés… todavía. De hecho, estoy trabajando para resolver esta limitación en la API de Caché ahora mismo.

Tus usuarios están primero

Las tecnologías subyacentes a las Aplicaciones Web Progresivas siguen madurando, pero incluso si no te interesa convertir tu sitio en una PWA, hay mucho más que puedes hacer actualmente para mejorar las experiencias de tus usuarios en lo que se refiere a archivos multimedia. Y, como con cualquier otra forma de diseño inclusivo, comienza centrándote en los usuarios que corren mayor riesgo de tener una experiencia horrible.

Traza distinciones entre los archivos multimedia críticos, lindos de tener, y superfluos. Elimina el código innecesario, y luego optimiza cuanto puedas cada uno de los activos restantes. Sirve tus archivos multimedia en varios formatos y tamaños, dando prioridad a las versiones más pequeñas primero para aprovechar al máximo la alta latencia y las conexiones lentas. Si tus usuarios dicen que quieren ahorrar datos, respeta eso y ten un plan alternativo. Almacena en caché de forma inteligente y con el mayor respeto por el espacio en disco de tus usuarios. Y, finalmente, inspecciona tus estrategias de almacenamiento en caché con frecuencia — especialmente cuando se trata de archivos multimedia grandes. Sigue estas instrucciones, y cada uno de tus usuarios — desde los que utilizan un JioPhone en una red móvil rural en la India hasta los que utilizan un portátil de alta gama para juegos conectados a fibra óptica de 10 Gbps en Silicon Valley — te lo agradecerán.

Si te gusta lo que hace A List Apart, ¡haz una inversión en apoyarnos! O síguenos en Twitter y Facebook. ¡Gracias!

--

--

Mariana Ramírez
A List Apart en Español

Graphic designer learning data wrangling skills for information design, data visualization & statistical analysis. Latin America Lead for @alistapartES