Caché en HTTP y Proxies

La caché de HTTP nos permite servir una página web mucho más rápido, evitando latencias y retrasos en el cliente y reduciendo el uso de nuestros servidores.
Caché en HTTP y Proxies

Generalmente cada una de las páginas de un sitio web está repleta de ficheros estáticos como imágenes, estilos CSS o código dinámico de Javascript. Cada vez que accedemos a una página web, el navegador tiene que descargar esos recursos para mostrar correctamente el sitio, una y otra vez. Siempre los mismos recursos por cada página que visitas dentro de un sitio web (si no usa la caché).

Esto es una seria desventaja por una serie de factores

  • El cliente tarda más en pedir todos estos recursos y esperar a que el servidor los entregue. La página aparentemente es más lenta pues tiene que realizar esta operación cada vez que cargas una página. Además, los ficheros ocupan espacio y gastan gigas a nuestros usuarios móviles.
  • El servidor web tiene que estar lidiando con peticiones a ficheros estáticos. Si tenemos una media de 5 recursos (siendo generoso) por página, tenemos que enviar por la red dichos recursos una y otra vez, lo que puede producir que peticiones imprescindibles se encolen. Esto produce que aumente el trabajo en nuestras máquinas, los recursos que necesita para mover todo ese tráfico y, derivado de esto, el coste que el proveedor de las máquinas nos cobre (si estás en cloud por ejemplo).

La caché de HTTP permite que nuestro navegador "se quede con una copia" del recurso para que no lo tenga que pedir en cada petición. Solo tendrías que pedir una imagen, un estilo de CSS o un fichero JS una sola vez cada mucho tiempo. Nuestros servidores dejarían de atender cientos de peticiones y nuestros clientes móviles nos agradecerían el ahorro de megas/gigas que esto supondría. Entendido el contexto, te presento un ejemplo donde lo vas a entender por ti mismo.

Ejemplo de caché HTTP

Si no conoces la web de MDN web docs, de Mozilla, ya la estás añadiendo a tus favoritos. Es una web muy útil para ampliar tus conocimientos (y refrescarlos y consultarlos cuando tienes una duda) sobre Javascript, CSS o los propios navegadores. Voy a entrar por primera vez con mi navegador, teniendo abierto el inspector de elementos, más concretamente la pestaña de Red (Network). En esta pestaña podrás ver los recursos que está pidiendo el navegador, el tamaño que ocupan (a nivel fichero y al ser enviados por la red) y el tiempo que ha tardado nuestro equipo en conseguir ese recurso. Descarga sin caché Descarga de MDN la primera vez. Sin caché En este caso, vemos como hay un montón de hojas de estilo que ocupan desde los 1.4 kB hasta algunos más pesados de 35kB, tardando en descargar entre 30 y 50 ms. En total, la página ha hecho 30 peticiones, transferido 789 lB y ha tardado 414ms en obtener todos los recursos a los que se hacía referencia en el DOM.

Estos ficheros, seguramente sean los mismos cuando navego a través de la web. Voy a volverme atrás y entrar de nuevo. Esta vez, los resultados del inspector de red han cambiado: Descarga con caché Descarga de MDN la segunda vez. Con caché ¡Anda! ¿Y esto? Si te fijas en los estilos sobre todo, verás que han pasado de 30-50kB a 0 y el tiempo de descarga de todos ellos es 0 milisegundos. Y a nivel general, la página se ha cargado en casi un 25% menos de tiempo. Eso es la caché de HTTP: hemos evitado tener que preguntar al servidor web por los mismos ficheros que ya hemos descargado la primera vez.

Pueden parecer cifras pequeñas, pero date cuenta que MDN tiene servidores bastante potentes y una serie de CDN para acelerar la descarga de ficheros, pero si en ellos (sitio de referencia) hemos ahorrado 300kB y un 25% del tiempo, ¿ qué pasará si aplicamos este concepto a una web amateur o semi-profesional que tarde 10 segundos en cargar ? (Créeme, las he visto). Pues básicamente, menos congestión, menos retardo y una ahorro de red considerable. Ah! y más rapidez ✈️.

Caché privadas y caché compartida. Tu navegador y un proxy

Existen dos tipos de cachés, las cachés finales de usuario o privadas (básicamente tu navegador) y las cachés compartidas, también llamadas proxies.

Lo más básico es el esquema directo: tu navegador se conecta directamente con el servidor que contiene la información o que la despachará. No hay intermediarios en la petición. La caché solo puede afectar a tu navegador. Esta es la caché privada.

Sin embargo, existen proveedores de datos que puedes contratar y cuyo funcionamiento consiste en anteponerse entre la conexión del navegador y el servidor final. Estos proveedores, como por ejemplo Cloudflare, reciben todas las conexiones del visitante y consultan si el recurso que se está pidiendo está en sus cachés. Si lo está, devuelven el contenido directamente (sin consultar con tu servidor). Si no lo estuviese, estos intermediarios conectarán con tu servidor, obtendrán la información y se la devolverán al usuario. Estas son las cachés públicas o proxies, que no diferencian al usuario: si un recurso se queda cacheado en ellas, le devolverán el mismo recurso a todos los visitantes que pregunten por él. Nunca jamás alojarías en este tipo de caché un documento bancario (pues corresponde a un cliente y nadie más debería verlo) pero si alojarías el logo de tu página web, que se debería distribuir a todos tus clientes. Tipos de cachéDiagramas de tipos de caché: sin caché, privados y públicos. Fuente: Mozilla MDN Generalmente estos intermediarios disponen de centros de datos por todo el mundo y responden mucho más rápido de lo que tu servidor podría hacerlo. Imagina que tienes un servidor en Irlanda y alguien te visita desde Madrid: estos intermediarios devolverán los estilos y los ficheros JS mucho más rápido desde el propio Madrid que si la información tiene que venir desde Irlanda. Lo importante es que funcionan como la caché de tu navegador, pero para todo el mundo, no solo para un cliente.

Aunque con matices, la directiva rápida y más sencilla es así:

  • Cosas generales, como estilos y scripts: caché en ambos tipos durante todo el tiempo posible.
  • Cosas privadas: solo el caché privado del usuario.

La cabecera de la caché de HTTP: Cache-Control

Pues toda esta magia se consigue con una sola cabecera: Cache-Control. Esta cabecera le dice a la caché cómo debe gestionar cada recurso. Puede indicar si se tiene que guardar en el navegador del usuario y/o en el proxy, cuanto tiempo se puede quedar allí, qué hacer cuando caduque, etc. Esta cabecera contiene una serie de directrices separadas por comas que actúan de diferentes formas en donde el orden no es importante pues afectan a aspectos diferentes.

La primera directiva más importante afecta a si debe ser cacheado o no y puede ser: no-store, no-cache, private o public. Te pongo algunos ejemplos:

//No cachear nada. Nunca. El navegador y el proxy se olvidan de guardarlo.
Cache-Control: no-store

//Cachea solo si es el navegador del usuario. El proxy no debe cachear
Cache-Control: private

//Cachea tanto el navegador como el proxy. Muy util para estáticos públicos
Cache-Control: public

//Cachea, pero no muestres hasta que estés seguro de que es un recurso válido
Cache-Control: no-cache

Mucho ojo con la última directiva: no-cache. Al contrario de lo que a priori puede parecer, no quiere decir que no se guarde en la caché. Al contrario: quiere decir que la respuesta puede ser cacheada pero, antes de utilizar este recurso, comprueba que es reciente y no ha caducado. Es decir, ante la negativa rotunda o el cacheo seguro, esta directiva trata de que el usuario siempre reciba una página o recurso actualizado. El navegador, antes de mostrar este recurso, preguntará al servidor si el propio recurso es reciente. Y si el servidor responde que sí, el recurso será cargado de la caché. Si por el contrario, el servidor dice que el recurso está caducado, el navegador cargará el recurso desde el servidor de origen y borrará el caducado de la caché. Luego te cuento más sobre esto.

La segunda directiva afecta al tiempo: ¿cuánto tiempo tiene que estar en el caché del navegador? ¿un minuto, una hora, un año? Tiene sentido plantearse esta pregunta cuando estamos utilizando esta cabecera. Date cuenta de que si le damos luz verde al navegador a que guarde un recurso, no volverá a preguntar por él hasta que haya caducado. Esto está bien, pero y si el recurso se modifica en el servidor, ese usuario no recibirá una copia actualizada hasta que su caché caduque (o borre los datos del navegador). Por eso, se añade la directiva max-age=[seconds]. Esta cabecera es relativa a la petición; es decir, el número de segundos desde que el navegador reciba una respuesta. Por ejemplo:

//Cachear en navegador y en el proxy durante 2 minutos (120 segundos)
Cache-Control: public, max-age=120

//Cachear solo en el navegador durante 1 hora (3600 segundos)
Cache-Control: private, max-age=3600

//Cachear un estático en navegador y proxy durante un año (31557600 seg)
Cache-Control: public, max-age=31557600

¿Y si quiero que el proxy lo guarde en la caché más tiempo que en el navegador? Es más, ¿y si no quiero que el usuario lo almacene en el navegador pero si el CDN? Para indicar el número de segundos del proxy usamos la directiva s-maxage=[seconds]. Esto funciona igual que la directiva max-age, pero solo afecta a caché intermedia.

//Que el cliente NO lo guarde en caché pero el proxy sí durante 1 hora
Cache-Control: public,max-age=31557600,s-maxage=3600

//Que el cliente lo guarde un año pero el proxy solo 1 hora
Cache-Control: public, max-age=
//Esta cabecera no tiene sentido, pues impera el private. El número de segundos no llega ni a ser leído.
Cache-Control: private,s-maxage=100

Es muy útil diferenciar entre max-age y s-maxage. Recursos que pueden cambiar muy a menudo pero quieres evitar saturar tu servidor son el ejemplo. Podemos hacer que el cliente no los almacene nunca y siempre los pida al intermediario. Este, que sí lo tiene cacheado, lo devuelve muy rápido. Cuando el contenido cambie, puedes avisar al CDN o proxy de que borre su caché y consiga el nuevo recurso. Este hará solo una petición cuando un cliente solicite el recurso en concreto y guardará el resultado en su caché de proxy. Hecho esto, todos los clientes posteriores, comenzarán a recibir el nuevo recurso. Solo tienes que borrar una caché en vez de todas las de tus visitantes.

Actualizar el contenido guardado en la caché

¡Genial! Ya casi lo tienes todo. Solo nos falta un asunto, ¿qué pasa cuando actualizamos un recurso que está cacheado? Pues una respuesta rápida es: depende. Si el navegador tiene guardado en caché un recurso, lamentablemente no lo podrás actualizar nunca hasta que caduque, que lo volverá a pedir. Hasta entonces, mala suerte 😥.

Pero esto no tiene porqué pasar si nos damos cuenta a tiempo. Existen varios recursos para forzar o engañar al navegador a que se descargue un nuevo contenido.

La primera de las formas es fácil: los parámetros de URL. El navegador funciona por rutas. Esto quiere decir que no es lo mismo main.js que main.js?version=1. Cuando añadimos parámetros a una URL deja de ser la misma y por lo tanto, forzamos al navegador (y proxy) a volver a descargarse el contenido. Por eso siempre te aconsejo que añadas parámetros a los ficheros que más tiempo estén cacheados como JS, CSS o imágenes.

Tú sitio web se ve agradable gracias a los CSS. Si algún día decides hacer cambios y los ficheros están cacheados en un proxy y/o navegador, solo los nuevos usuarios que no hayan recibido una copia de esos estilos verán los cambios. Por eso, siempre te aconsejo añadir algún parámetro diferenciados a los js, css o imágenes, por si algún día haces un cambio y quieres distribuirlo a todo el mundo. Así que a partir de ahora, en vez de llamar a tus ficheros directamente, añade un parámetro de versión:

<!-- Antes -->
<link rel="stylesheet" href="miestilo.css"/>
<!-- Ahora -->
<link rel="stylesheet" href="miestilo.css?v=1"/>

Lo bueno de esto es que el parámetro no afecta al nombre del fichero. Todo lo que vaya después de la interrogación es independiente del nombre. En el ejemplo anterior, siempre se hace referencia al fichero 'miestilo.css', así que no te líes a cambiar el nombre de tus ficheros. Con añadir el parámetro en la URL vale.

De hecho, si ya eres un profesional más avanzado y has probado React o Angular, te habrás dado cuenta de que los CSS y los JS que obtienes tienen incluidos unos "codiguitos" estilo main.54ds6.js. Esto es precisamente para saltarse la caché cuando haya un cambio y solo descargar los contenidos nuevos.

Otra opción que tienes si no quieres añadir parámetros es usando la directiva no-cache. Recuerdas que te dije que no significaba lo que daba a entender por el nombre. Recapitulando, los recursos con la directiva no-cache permiten al navegador cachear un recurso pero siempre comprobando que no hay una versión más reciente. Lo malo de esta directiva es que siempre hay una petición al servidor. Lo bueno, es que si el recurso no ha cambiado, la respuesta será un 304 Not modified y sin cuerpo; traduciendo, que en vez de kB de tamaño, apenas serán unos bytes. Siempre es más rápido enviar bytes que kilobytes.

¿Y cómo sabe el navegador o el servidor si el recurso está actualizado 🤔? Pues hay varias formas de hacerlo: usando la cabecera If-Modified-Since o usando la cabecera ETag.

La cabecera If-Modified-Since (que se traduce por "si no ha sido modificado desde") la manda el navegador. Es muy útil cuando se está pidiendo estáticos (que existen en el servidor como ficheros). El navegador envía la petición para comprobar si el recurso que tiene en caché es el último igual que con cualquier petición, pero añade esta cabecera indicando la fecha y hora en la que guardó el recurso en la caché. El servidor, comprobará la fecha del fichero y si no ha cambiado desde entonces, devolverá un 304 Not Modified. El navegador cuando lee esta respuesta entiende que sí tiene la última versión, por lo que puede usarla. Si por el contrario el servidor detecta que hay una versión más reciente, devolverá un 200 Ok y el propio recurso en la misma petición. El navegador entiende que debe borrar el recurso que tiene guardado en su caché y reemplazarlo por este nuevo que está enviando el servidor. Este proceso se repetirá siempre tal cual. Según el navegador detecte que necesita ese recurso y comprueba que existe en la caché con la directiva no-cache, hará la petición esperando un 200 o un 304.

La otra posibilidad que tienes es usar la cabecera ETag. Funciona de forma similar a If-Modified-Since, pero en este caso no contiene fechas, sino cadenas de texto. Cuando el servidor envía un recurso le añade la cabecera ETag cuyo valor es aleatorio pero permite identificar el recurso (la firma de su contenido mediante algún hash, la fecha de generación, la versión, etc). Una vez que el navegador quiere comprobar si el recurso es la última versión envía la petición con la cabecera ETag según la envió el servidor. El servidor comprueba así si el recurso ha cambiado de versión y la cadena de texto coincide con la que envió el cliente. Si coincide, el servidor devuelve un 304; si no, un 200. No deberías tener que preocuparte de generar un ETag, los principales servidores web lo harán por ti. Quédate con el concepto de que el servidor sabe a través de ese ETag a qué versión del fichero se refiere.

Cómo aplicar la caché HTTP a mi web

Pues ya tienes toda la teoría que necesitas para empezar y para volar durante un tiempo. Pero, ¿y la práctica?. Pues depende del servidor web que estés utilizando, aunque seguramente sea Apache o Nginx. Si es Apache, tienes toda la información sobre caché en la web oficial (versión 2.4) o en muchos blogs, pero básicamente es usar el módulo Header de Apache (aunque puedes usar expires, pero no me hace mucha gracia; prefiero establecerlo a mano):

<FilesMatch "\.(ico|pdf|flv|jpg|jpeg|png|gif|js|css|swf)$">
  Header set Cache-Control "max-age=63072000, public"
</FilesMatch>

// O...

<Directory "/private">
 Header set Cache-Control "max-age=300, private"
</Directory>

En Nginx creo que es más sencillo (aunque siempre me gustó más Nginx, por lo que mi opinión es subjetiva). Entra en el fichero de configuración de tu sitio y añade cabeceras según el tipo de ruta.

location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico)$ {
 add_header Cache-Control "public, max-age=3600, s-maxage=10000";
}

Espero que te haya quedado un poco más claro el tema de la caché y cómo puede ayudarte a optimizar tu sitio web y ayudarte con tu sitio web. Es más, estoy pensando que podría añadir caché al proyecto del Blog en Ghost que te conté en este artículo. No sé... a lo mejor hacemos un nuevo capítulo de esa serie 😏.

Que tengas un feliz coding !