Lección 3 de 412 min
Comprimir y cachear: Brotli, Gzip, HTTP y CDN
Cómo reducir cada respuesta y evitar descargarla otra vez con cabeceras de caché y CDN.
Ya has reducido el JavaScript que envías. Quedan dos palancas más, y casi siempre son las más baratas: que cada respuesta pese menos al viajar y que el navegador no tenga que volver a pedir lo que ya tiene. Un archivo de 400 kB puede viajar en 100 kB comprimido, y en la segunda visita no viajar en absoluto.
En esta lección verás la compresión con Gzip y Brotli, la caché del navegador con la cabecera Cache-Control y la caché de una CDN. Al final podrás componer tus propias cabeceras y ver, visita a visita, qué hacen el navegador y la CDN.
Comprimir las respuestas
El navegador anuncia qué sabe descomprimir con Accept-Encoding: gzip, br. El servidor elige uno, comprime la respuesta y lo indica con Content-Encoding. El navegador la descomprime sin que tu código se entere.
Gzip
La compresión clásica de la web: rápida, universal y suficiente para casi todo.
- Cabecera:
- Content-Encoding: gzip
- Niveles:
- 1 a 9
Gzip usa el algoritmo DEFLATE, que busca fragmentos repetidos y los sustituye por referencias más cortas. Los formatos de texto de la web (HTML, CSS, JavaScript, JSON, SVG) están llenos de repeticiones, así que encogen muchísimo.
Lo entiende cualquier cliente HTTP, tiene niveles del 1 al 9 y comprime rápido incluso en los niveles medios, así que puedes usarlo para comprimir cada respuesta al vuelo, también las que genera tu API.
Úsalo cuando
Como base en cualquier servidor, y como alternativa para los clientes que no aceptan Brotli.
Ten en cuenta
No comprimas lo que ya está comprimido: JPEG, PNG, WebP, AVIF, WOFF2, ZIP o vídeo solo gastarían CPU para nada. Y envía Vary: Accept-Encoding, para que una caché intermedia no sirva la versión comprimida a quien no la pidió.
gzip on;
gzip_comp_level 6;
gzip_min_length 1024; # con archivos diminutos no compensa
gzip_vary on; # añade Vary: Accept-Encoding
gzip_types text/css application/javascript application/json image/svg+xml;
# text/html se comprime siempre que gzip está activadoBrotli
Comprime el texto más que Gzip; ideal para precomprimir los archivos estáticos.
- Cabecera:
- Content-Encoding: br
- Niveles:
- 0 a 11
- Solo por:
- HTTPS
Brotli, creado por Google, consigue archivos de texto algo más pequeños que Gzip, en parte gracias a un diccionario incorporado con palabras y fragmentos muy comunes en la web. Todos los navegadores modernos lo soportan, aunque solo lo anuncian en conexiones HTTPS.
Tiene niveles del 0 al 11, y los más altos son lentos. Por eso la estrategia habitual es doble: precomprimir los archivos estáticos al máximo durante la build y servir el .br ya hecho, y usar un nivel medio para las respuestas dinámicas.
Empieza a aparecer un tercer candidato, Zstandard (zstd), creado en Meta, que algunos navegadores y CDN recientes ya aceptan. Comprueba el soporte antes de contar con él.
Úsalo cuando
Para el JavaScript, el CSS y los SVG de tu build, precomprimidos, y para el HTML si tu servidor o tu CDN lo ofrecen.
Ten en cuenta
Comprimir al nivel 11 en cada petición quema CPU y retrasa el primer byte. Y muchas CDN comprimen por su cuenta: revisa qué hace la tuya antes de duplicar el trabajo.
# En la build: cada archivo estático se comprime una vez, al máximo
brotli --best dist/assets/*.js dist/assets/*.css
# nginx con el módulo ngx_brotli: sirve el .br si existe
# y el navegador lo acepta, y si no, el .gz
brotli_static on;
gzip_static on;La caché del navegador
La petición más rápida es la que no se hace. Con las cabeceras adecuadas, el navegador reutiliza lo que ya descargó sin preguntar o, como mucho, pregunta si ha cambiado.
Cache-Control
La cabecera que dice quién puede guardar una respuesta y durante cuánto tiempo.
- Fresca:
- Se usa sin preguntar
- no-cache:
- Guarda, pero pregunta
Con max-age=N, la respuesta está fresca durante N segundos: mientras lo esté, el navegador la reutiliza sin hacer ninguna petición. Cuando caduca, pasa a estar obsoleta y tiene que comprobar si sigue valiendo.
Dos directivas se confunden constantemente. no-cache no significa «no guardar»: significa «guárdala, pero pregunta al servidor antes de cada uso». La que prohíbe guardar es no-store, pensada para datos sensibles. Además, private permite guardarla solo al navegador del usuario, nunca a una caché compartida, e immutable le dice al navegador que, mientras esté fresca, no la revalide ni siquiera si el usuario recarga la página.
Úsalo cuando
En todas las respuestas. Decide para cada tipo de recurso: archivos estáticos con hash, HTML, respuestas de la API, datos personales.
Ten en cuenta
Si no envías Cache-Control, los navegadores pueden aplicar una caché «heurística» y guardar la respuesta un tiempo calculado a partir de Last-Modified. Sé siempre explícito.
# Archivos con hash en el nombre: un año, sin revalidar ni al recargar
Cache-Control: public, max-age=31536000, immutable
# HTML: guárdalo, pero pregunta siempre si ha cambiado
Cache-Control: no-cache
# Datos personales o sensibles: no lo guardes nunca
Cache-Control: no-storeETag y respuestas 304
Preguntar «¿ha cambiado?» y recibir un «no» de pocos bytes en lugar del archivo entero.
- Pregunta:
- If-None-Match
- Respuesta:
- 304, sin cuerpo
Que una copia haya caducado no significa que esté desactualizada. El ETag es un identificador de la versión de una respuesta, a menudo un hash de su contenido. Cuando la copia caduca, el navegador pregunta con If-None-Match y ese valor; si nada ha cambiado, el servidor responde 304 Not Modified, sin cuerpo, y el navegador reutiliza su copia.
Si sí ha cambiado, responde 200 con la versión nueva y su nuevo ETag. La alternativa más antigua es por fechas, con Last-Modified e If-Modified-Since.
Úsalo cuando
En recursos que cambian de vez en cuando y cuyo nombre no cambia: el HTML, un catálogo en JSON, una imagen de perfil.
Ten en cuenta
Un 304 sigue costando un viaje de ida y vuelta: con mucha latencia, una copia fresca, que no hace ninguna petición, es mucho más rápida. Y si tienes varios servidores, todos deben generar el mismo ETag para el mismo archivo, o las revalidaciones fallarán.
GET /catalogo.json HTTP/1.1
If-None-Match: "33a64df5"
HTTP/1.1 304 Not Modified
ETag: "33a64df5"
Cache-Control: max-age=60Nombres de archivo con hash
Si el contenido cambia, cambia el nombre: así puedes cachear un año sin miedo.
- También llamado:
- Cache busting
El dilema es este: con un max-age largo, las visitas repetidas vuelan, pero los usuarios no verán tus cambios; con uno corto, verán los cambios, pero todo irá más lento. La solución es poner en el nombre del archivo un hash de su contenido: main-7KQ2XG3B.js.
Si cambias una sola línea, la build genera otro nombre, y el archivo viejo simplemente no se vuelve a pedir. Esos archivos pueden llevar max-age de un año e immutable. Lo que no puede cachearse tanto es el HTML que los enlaza, que se sirve con no-cache para que siempre apunte a los nombres nuevos. Vite, webpack y el CLI de Angular lo hacen solos en las builds de producción.
Úsalo cuando
Para todo el JavaScript, el CSS, las fuentes y las imágenes que genera tu build.
Ten en cuenta
Tras desplegar, mantén un tiempo los archivos antiguos: quien tenga la página vieja abierta pedirá los chunks lazy con los nombres viejos y, si ya no existen, la navegación fallará.
<!-- index.html se sirve con Cache-Control: no-cache -->
<link rel="stylesheet" href="/assets/styles-5INURTSO.css">
<script type="module" src="/assets/main-7KQ2XG3B.js"></script>
<!-- Todo /assets/ se sirve con max-age=31536000, immutable.
Si cambias una línea, la build genera main-QX81LM0A.js
y el index.html nuevo apunta a él. -->La caché de la CDN
Una CDN es una caché compartida entre tus usuarios y tu servidor, repartida por centros de datos de todo el mundo, como viste en el módulo de cloud. Lo que pide un usuario puede servirse a los siguientes sin llegar a tu servidor.
s-maxage, purga y Vary
Cuánto guarda la CDN, cómo borrar lo que ha cambiado y cuándo guardar varias versiones.
- Solo para:
- Cachés compartidas
- Invalidar:
- Purga
s-maxage fija la vida de la respuesta solo en las cachés compartidas, como la CDN, y allí manda sobre max-age. Así puedes hacer que la CDN guarde el HTML una hora mientras los navegadores lo revalidan siempre: las revalidaciones las contesta la CDN y tu servidor descansa.
Cuando algo cambia antes de caducar, lo purgas: lo borras de la CDN por URL, por etiqueta o entero, y la siguiente petición va al origen. Y Vary indica qué cabeceras de la petición cambian la respuesta, para que la CDN guarde una versión por cada valor: Vary: Accept-Encoding guarda una copia con Brotli y otra con Gzip.
Úsalo cuando
Para el HTML y las respuestas de API públicas que son iguales para todos los usuarios y que puedes purgar cuando cambian.
Ten en cuenta
Nunca guardes en una caché compartida una respuesta personalizada: marca esas respuestas con private. Cada CDN tiene sus reglas por defecto, y muchas no guardan el HTML salvo que se lo pidas. Y Vary: Cookie o Vary: User-Agent crean tantas variantes que la caché casi nunca acierta.
# El navegador revalida siempre; la CDN lo guarda una hora
Cache-Control: public, max-age=0, s-maxage=3600
Vary: Accept-Encoding
# Tras editar un artículo, purga solo esa URL (API de Cloudflare)
curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONA/purge_cache" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"files": ["https://tienda.com/blog/rebajas-de-otono"]}'stale-while-revalidate
Servir al instante la copia caducada mientras se trae la nueva en segundo plano.
- Hermano:
- stale-if-error
Cuando una copia caduca, el siguiente usuario normalmente espera a que la caché consulte al origen. Con stale-while-revalidate=N, durante N segundos después de caducar, la caché sirve la copia vieja al instante y, a la vez, pide la nueva en segundo plano. Ese usuario no espera, y el siguiente ya recibe la versión fresca.
Muchas CDN lo soportan, y algunos navegadores lo aplican también en su propia caché. Su hermano, stale-if-error, permite servir la copia vieja si el origen falla.
Úsalo cuando
Para contenido que cambia pero tolera unos minutos de retraso: la portada, un listado de productos, un artículo.
Ten en cuenta
Alguien verá contenido desactualizado durante un rato. No lo uses para precios en el proceso de pago, stock o cualquier dato que deba ser exacto.
# La CDN la sirve 5 minutos sin preguntar. Después, y durante un día,
# sirve la copia vieja al instante mientras trae la nueva
Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=86400Interactivo
La línea de tiempo de la caché
Compón las cabeceras de un archivo y mira qué hacen el navegador y la CDN en cada visita del mismo usuario.
Cache-Control: max-age=3600
ETag: "33a64df5"Visita en t = 0
NavegadorDescarga completaPrimera visita: no tiene nada en caché.CDNDescarga completaEstá vacía: pide el archivo completo al origen y lo guarda.Visita en 30 s
NavegadorSirve de cachéSu copia tiene 30 s y max-age es 1 h: sigue fresca. Ni siquiera hace la petición.CDNNo intervieneNo recibe nada: la petición no sale del navegador.Visita en 10 min
NavegadorSirve de cachéSu copia tiene 10 min y max-age es 1 h: sigue fresca. Ni siquiera hace la petición.CDNNo intervieneNo recibe nada: la petición no sale del navegador.Visita en 2 h
NavegadorRevalida con 304Su copia tiene 2 h y max-age es 1 h: ha caducado. Pregunta conIf-None-Match, recibe un 304 y reutiliza su copia.CDNRevalida con 304Su copia tiene 2 h y max-age es 1 h: ha caducado. Pregunta conIf-None-Matchy el origen responde 304, sin cuerpo.Visita en 2 días
NavegadorRevalida con 304Su copia tiene 46 h y max-age es 1 h: ha caducado. Pregunta conIf-None-Match, recibe un 304 y reutiliza su copia.CDNRevalida con 304Su copia tiene 46 h y max-age es 1 h: ha caducado. Pregunta conIf-None-Matchy el origen responde 304, sin cuerpo.
En 5 visitas, el origen envió el archivo completo 1 vez y respondió 2 veces con un 304.
Prueba los atajos de arriba o cambia max-age para ver cuándo caduca cada copia.
Simplificado: un solo usuario, el archivo nunca cambia, el servidor no envía Last-Modified y stale-while-revalidate solo se aplica en la CDN, que es donde más se usa.
Interactivo
Gzip frente a Brotli
Activa o desactiva opciones para compararlas cara a cara.
| Característica | Gzip | Brotli |
|---|---|---|
| Compresión de texto | Buena | Mejor: archivos algo más pequeños |
| Velocidad al comprimir | Rápida en todos los niveles | Lenta en los niveles más altos |
| Soporte | Universal, también por HTTP | Navegadores modernos, solo por HTTPS |
| Niveles | 1 a 9 | 0 a 11 |
| Uso ideal | Respuestas dinámicas y compatibilidad total | Archivos estáticos precomprimidos al máximo |
Ponlo en práctica
Saber qué es cada cosa es la mitad del trabajo. La otra mitad es reconocer cuándo usarla.
Interactivo
¿Qué usarías?
Lee cada situación y elige la opción que mejor la resuelve. Fallar aquí no cuenta: es para practicar.
Tu API genera respuestas JSON distintas en cada petición y algunos de sus clientes son programas antiguos que solo entienden la compresión clásica.
Para recordar
- Comprime todo el texto: Gzip al vuelo para las respuestas dinámicas y Brotli precomprimido al máximo para los estáticos.
no-cachesignifica «guarda, pero pregunta»;no-store, «no guardes nunca».- Con ETag, una copia caducada se revalida con un 304 de pocos bytes en lugar de descargar el archivo entero.
- Archivos con hash en el nombre y
max-agede un año conimmutable; el HTML que los enlaza, conno-cache. s-maxagecontrola la CDN sin afectar al navegador, ystale-while-revalidateevita que alguien espere al origen cuando caduca.
Comprueba lo aprendido
Cuestionario
Responde las 5 preguntas para completar la lección.
1.¿Qué significa
Cache-Control: no-cache?2.Un archivo llega con
Cache-Control: max-age=3600y un ETag. Dos horas después, el usuario vuelve. ¿Qué hace el navegador?3.¿Por qué no se comprime con Brotli al nivel 11 cada respuesta dinámica?
4.¿A quién afecta
s-maxage?5.¿Por qué un archivo como
main-7KQ2XG3B.jspuede cachearse un año conimmutable?