wandres.dev
EL EDGE Y LA CDN · Acercar el byte al usuario

Qué se puede cachear en el borde y qué no

Los tres niveles de cacheabilidad, las directivas que una caché compartida lee de verdad, las cuatro salidas al problema de la personalización, y cómo medir la tasa de aciertos.

⏱ 19 min

La pregunta “¿esto se puede cachear?” está mal planteada. La cacheabilidad no es una propiedad que se descubre mirando una respuesta: es una propiedad que se diseña, separando lo que es igual para todos de lo que no. Casi cualquier página tiene un noventa por ciento cacheable y un diez por ciento que no lo es, y toda la ingeniería consiste en no dejar que el diez por ciento decida por el noventa.

🎯 Al terminar esta lección sabrás
  • Clasificar cualquier respuesta en uno de los tres niveles de cacheabilidad.
  • Escribir las cabeceras que una caché compartida interpreta, distinguiéndolas de las del navegador.
  • Aplicar las cuatro salidas al problema de la personalización.
  • Medir la tasa de aciertos por ruta y diagnosticar por qué una respuesta no se cachea.

Los tres niveles

Nivel uno: recursos inmutables. Ficheros cuyo nombre contiene un hash de su contenido. Como el nombre cambia cuando cambia el contenido, la respuesta para ese nombre nunca cambia y se puede cachear para siempre, en el navegador y en el borde.

Cache-Control: public, max-age=31536000, immutable

immutable no es redundante con el año de caducidad: le dice al navegador que ni siquiera revalide cuando el usuario pulsa recargar, que es cuando un max-age largo se ignora. Sin ella, cada recarga manual dispara una petición condicional por cada recurso.

Nivel dos: contenido público con caducidad. HTML que es igual para todo el mundo pero que cambia con el tiempo. El patrón correcto separa lo que hace el navegador de lo que hace el borde:

Cache-Control: public, max-age=0, must-revalidate
CDN-Cache-Control: public, s-maxage=3600, stale-while-revalidate=86400, stale-if-error=604800

El navegador no guarda copias que puedan quedarse viejas —revalida siempre— y el borde guarda una hora, sirve contenido caducado durante un día mientras revalida por detrás, y hasta una semana si el origen está caído. La cabecera CDN-Cache-Control existe precisamente para poder dar instrucciones distintas a las cachés intermedias sin afectar al navegador; si tu proveedor no la soporta, la alternativa es s-maxage, que las cachés compartidas leen y el navegador ignora.

Nivel tres: respuestas personales. Todo lo que depende de quién pregunta.

Cache-Control: private, no-store

Aquí conviene precisar dos directivas que se confunden constantemente. no-cache sí permite guardar la respuesta, pero obliga a revalidarla antes de cada uso: es “guarda pero pregunta”. no-store prohíbe guardarla en cualquier sitio, incluido el disco del navegador: es “no la escribas en ninguna parte”. Para una página con datos bancarios quieres no-store; para una página pública que cambia a menudo quieres no-cache, que te da revalidaciones baratas con ETag. Usar no-store por costumbre en respuestas públicas desperdicia toda la caché del navegador, y además hace la página inelegible para la caché de retroceso.

Lo que rompe la caché sin que te enteres

Tres cosas que convierten una respuesta perfectamente cacheable en una que no se cachea nunca, y ninguna produce un error.

La cabecera Vary mal usada. Vary le dice a la caché que la respuesta depende de esas cabeceras de la petición, así que hay que guardar una copia por cada valor distinto. Vary: Accept-Encoding es correcto y produce dos o tres variantes. Vary: User-Agent produce miles, porque hay miles de cadenas de agente distintas, y en la práctica anula la caché. Vary: Cookie es todavía peor: una variante por usuario.

Una cabecera Set-Cookie en la respuesta. Muchas cachés compartidas se niegan a guardar una respuesta que establece una cookie, y con razón: guardarla significaría entregar la cookie de una persona a otra. Un middleware de sesión que crea una cookie en cada petición, aunque sea anónima, desactiva la caché de todo el sitio. Es una de las causas más frecuentes de “tenemos CDN y no acierta nunca”.

Cookies en la petición. Algunas configuraciones desactivan la caché cuando la petición trae ciertas cookies. Si tu herramienta de analítica pone una cookie en el primer visitante, a partir de la segunda página todas las peticiones la llevan, y todas se van al origen. El remedio es configurar explícitamente qué cookies afectan a la caché y cuáles se ignoran, que es parte de la clave de caché y se trata en la clave de caché.

Las cuatro salidas a la personalización

Cuando una página pública tiene un trozo personal, hay cuatro formas de separarlos. Están ordenadas de menos a más infraestructura.

Uno: rellenar en el cliente desde una cookie. El HTML cacheado trae un hueco y un script diminuto lo rellena leyendo una cookie que ya viaja en cada petición.

<span id="saludo" hidden></span>
<script>
  (function () {
    var m = document.cookie.match(/(?:^|; )nombre=([^;]*)/);
    if (!m) return;
    var n = document.getElementById('saludo');
    n.textContent = 'Hola, ' + decodeURIComponent(m[1]);
    n.hidden = false;
  })();
</script>

Es la solución más humilde y la correcta para el noventa por ciento de los casos reales, que son un nombre en la esquina y un contador de carrito. Cuesta trescientos bytes y convierte una página no cacheable en una cacheable para siempre. El detalle importante es el hidden inicial: sin él, hay un instante con el hueco vacío y un salto de layout.

Dos: pedir la parte personal aparte. Una petición a un endpoint pequeño que devuelve solo lo personal, mientras el HTML cacheado ya se está pintando. Añade una ida y vuelta, pero fuera del camino crítico del LCP.

Tres: mandarla en el mismo flujo, al final. El HTML público sale por streaming desde el borde y el trozo personal se añade al final con el mecanismo de contenido fuera de secuencia. Necesita que la ruta no esté cacheada como fichero, sino generada, pero permite cachear en el borde todo lo que no es personal.

Cuatro: sustituir en el borde. Una función en el nodo de borde toma la respuesta cacheada y reescribe los trozos personales sobre la marcha, sin ir al origen. Varias plataformas ofrecen reescritura de HTML en streaming, que aplica transformaciones a medida que los bytes pasan sin acumular el documento.

// Reescritura en streaming en el borde. Sintaxis de Cloudflare Workers;
// otras plataformas tienen equivalentes.
export default {
  async fetch(peticion) {
    const respuesta = await fetch(peticion);   // sale de la cache del borde
    const nombre = leerNombreDeCookie(peticion);
    if (!nombre) return respuesta;

    return new HTMLRewriter()
      .on('#saludo', {
        element(el) {
          el.setInnerContent('Hola, ' + nombre);
          el.removeAttribute('hidden');
        },
      })
      .transform(respuesta);
  },
};

Es la opción más potente y la que más infraestructura exige. Su ventaja sobre la primera es que funciona sin JavaScript en el cliente y no produce ningún parpadeo.

La regla para elegir: empieza por la uno. Solo si el trozo personal es grande, indexable o crítico para el primer pintado, sube de nivel. Mucha gente implementa la cuatro para resolver un problema que la uno cerraba en una tarde.

Medir la tasa de aciertos

El número que resume tu estrategia de borde es el porcentaje de peticiones servidas sin tocar el origen, por ruta. El agregado engaña: un 92 % global puede ser un 99 % en imágenes y un 3 % en HTML, que es justo lo que importa.

Las cabeceras de diagnóstico varían por proveedor, pero todas tienen alguna: un estado de caché con valores del tipo acierto, fallo, caducado, revalidado o excluido, y la cabecera estándar Age, que indica cuántos segundos lleva esa copia en la caché. Age: 0 en todas las respuestas de una ruta significa que nunca acierta.

# Dos peticiones seguidas: la segunda deberia acertar.
curl -sI https://ejemplo.com/producto/42 | grep -iE 'age|cache|x-cache|cf-cache'
curl -sI https://ejemplo.com/producto/42 | grep -iE 'age|cache|x-cache|cf-cache'

Cuando una ruta no acierta y debería, el diagnóstico sigue este orden, que resuelve casi todos los casos:

  1. ¿La respuesta lleva Set-Cookie? Quítala de las respuestas cacheables.
  2. ¿Lleva Cache-Control: private, no-store o max-age=0 sin s-maxage? Corrige la cabecera.
  3. ¿Lleva un Vary con algo de alta cardinalidad? Quítalo.
  4. ¿La petición trae cookies que el proveedor considera excluyentes? Configura cuáles ignorar.
  5. ¿La URL tiene parámetros de campaña que la hacen única? Normalízalos en la clave.
La cacheabilidad no se descubre: se diseña, y la unidad de diseño no es la respuesta sino el trozo

El error de fondo, del que salen todos los demás de esta lección, es tratar la cacheabilidad como un atributo que una respuesta tiene o no tiene, y que se averigua examinándola. Con esa mentalidad el razonamiento es siempre el mismo: miro la página, veo que hay un nombre de usuario, concluyo que la página es personal, y le pongo private. Y entonces cada petición de cada usuario recorre el planeta hasta tu origen, ejecuta tu servidor y consulta tu base de datos, para producir un documento que es idéntico al de todos los demás salvo por doce caracteres. La mentalidad correcta es la contraria: la cacheabilidad es un objetivo de diseño, y el trabajo de ingeniería consiste en empujar la frontera de lo no cacheable hasta que sea lo más pequeña posible. No preguntas “¿es cacheable esta página?”, preguntas “¿cuál es la unidad más pequeña que contiene todo lo personal, y cómo la separo del resto?”. Esa pregunta tiene siempre respuesta, y su respuesta casi siempre es sorprendentemente pequeña. Fíjate en que las cuatro salidas de esta lección son la misma idea aplicada con cuatro tecnologías distintas: en el cliente, en una petición aparte, en el flujo o en el borde. La elección entre ellas es táctica; lo que no es táctico es la decisión previa de separar. Y hay un corolario organizativo que conviene tener presente, porque explica por qué esto se hace mal tan a menudo: la separación exige que alguien mire la página completa y decida dónde está la frontera, y esa persona no es el desarrollador del componente del avatar ni el del listado. La frontera de personalización es una decisión de arquitectura de la ruta, y en muchos equipos no tiene dueño. Por eso el síntoma más habitual no es una mala configuración de CDN sino la ausencia de cualquier configuración: nadie decidió que la portada fuera cacheable, así que no lo es, y la infraestructura carísima que se contrató para acelerarla está sirviendo únicamente los logotipos.

⚔️ Sube tu tasa de aciertos
  1. Mide la tasa de aciertos por ruta durante una semana y ordena por número de peticiones que llegan al origen.
  2. Coge la ruta que más origen consume y aplica el diagnóstico de cinco pasos.
  3. Comprueba si alguna respuesta cacheable lleva Set-Cookie. Es la causa más frecuente y la más fácil de arreglar.
  4. Busca todos los Vary de tu configuración y calcula cuántas variantes genera cada uno.
  5. Implementa la salida número uno —rellenar desde cookie— en una ruta que hoy es privada por un saludo, y vuelve a medir.