12k
All articles

Gzip vs Brotli vs Zstd para comprimir recursos web

Brotli, gzip o zstd para recursos web: cuándo usar cada uno, cómo influyen los niveles de compresión y cómo precomprimir archivos estáticos.

OpenReplay Team
OpenReplay Team
Gzip vs Brotli vs Zstd para comprimir recursos web

Usa Brotli para recursos de texto estáticos, gzip como alternativa universal y zstd para respuestas dinámicas en las que controles tanto el servidor como la CDN.

Muchos bundles siguen sirviéndose con gzip porque una herramienta de build o una configuración por defecto del hosting lo eligió hace años, y nadie ha vuelto a revisar el algoritmo ni el nivel desde entonces. El resto de este artículo explica por qué el contenido estático y el dinámico son problemas distintos, por qué el nivel de compresión suele cambiar el resultado más que el nombre del algoritmo, cómo precomprimir en tiempo de build, cómo negocian el navegador y el servidor, y qué conviene dejar sin comprimir.

Puntos clave

  • Brotli en su nivel máximo es la opción correcta para JavaScript y CSS estáticos porque el coste de una compresión lenta se paga una sola vez en el build, no por cada petición.
  • Zstd encaja en las respuestas dinámicas: Cloudflare midió que comprimía un 42 % más rápido que Brotli quedándose cerca de él en tamaño, con medias de 2,56:1 para gzip, 2,86:1 para zstd y 3,08:1 para Brotli.
  • El nivel importa tanto como el nombre del algoritmo: cambiar a Brotli dejando un nivel rápido por defecto significa renunciar a parte del ahorro.
  • En nginx, gzip_comp_level tiene el valor por defecto 1 y brotli_comp_level de ngx_brotli, 6; ninguno de los dos es el máximo.
  • La negociación de la codificación ocurre íntegramente en las cabeceras Accept-Encoding y Content-Encoding, así que cambiar de algoritmo es un ajuste de servidor o de CDN, no un cambio en el código de la aplicación.

Brotli vs Gzip vs Zstd: la recomendación

Brotli es el valor por defecto adecuado para JavaScript y CSS estáticos porque la compresión ocurre una sola vez durante el build, de modo que los niveles más altos y lentos no cuestan nada en tiempo de petición.

Gzip se mantiene en el stack como alternativa porque todos los navegadores lo incluyen en Accept-Encoding; no es la mejor opción para nada, pero es la única que nunca falla.

Zstd corresponde a las respuestas dinámicas, donde el coste de compresión se paga en cada petición, porque alcanza una ratio cercana a la de Brotli con una fracción del tiempo de CPU.

Estático vs dinámico: ¿cuándo se paga el coste de compresión?

La decisión entre algoritmos es una decisión sobre cuándo se paga el coste de CPU. Un bundle estático se comprime una vez por release y se sirve miles de veces, así que solo importa la ratio y la velocidad de compresión es irrelevante. Una página HTML renderizada por petición se comprime en cada acceso, así que la velocidad de compresión pasa a ser una preocupación de latencia y de coste de servidor a la par que la ratio.

El anuncio de soporte de Zstandard de Cloudflare de septiembre de 2024 aporta las cifras para el caso dinámico. En una prueba del Q3 de 2024 que cambió el tráfico del plan Free de Brotli a zstd durante 24 horas, con zstd en su nivel por defecto 3 y sin indicar los niveles de Brotli y gzip, zstd comprimió un 42 % más rápido que Brotli y quedó cerca de igualarlo en tamaño. Las medias medidas fueron 2,56:1 para gzip, 2,86:1 para zstd y 3,08:1 para Brotli, y la conclusión de la propia Cloudflare fue que zstd encaja mejor en respuestas dinámicas, HTML incluido.

Veredicto: Brotli gana donde la ratio es el único criterio, lo que describe a todos los recursos estáticos. Zstd gana donde la compresión se ejecuta por petición.

El nivel de compresión importa tanto como el algoritmo

Todos los algoritmos exponen un control de nivel, y el nivel a menudo mueve el resultado más que cambiar de algoritmo. Brotli usa niveles de 0 a 11 en ngx_brotli, gzip de 1 a 9 en nginx, y la CLI de zstd de 1 a 19 con un valor por defecto de 3, más de 20 a 22 detrás de --ultra.

Cuánto mueve el nivel el resultado depende por completo de la entrada, así que las cifras publicadas de antes y después del bundle de otra persona dicen muy poco sobre el tuyo. Pasa tus archivos por la herramienta de comparación de compresión, que comprime con gzip, Brotli y zstd a cualquier nivel en el navegador, y compara los tamaños tú mismo.

Los valores por defecto explican por qué tantos sitios sirven en el extremo rápido. En nginx, gzip_comp_level tiene por defecto 1, el más rápido de los niveles 1 a 9. En ngx_brotli, brotli_comp_level tiene por defecto 6 de 0 a 11. Ninguno es el máximo, y ambos son razonables para compresión al vuelo, que es justamente el contexto equivocado para los recursos estáticos. Cualquiera que sea el nivel que elijas, se paga en el lado que comprime. El propio README del proyecto zstd señala que la decodificación corre aproximadamente a la misma velocidad sea cual sea el nivel que produjo el archivo, algo que también se cumple para zlib y lzma.

Veredicto: fija el nivel de forma deliberada. Máximo para todo lo precomprimido; un nivel intermedio para todo lo que se comprime por petición.

¿Cómo se precomprimen los recursos en tiempo de build?

Precomprimir significa que el paso de build escribe un archivo hermano .br, .gz y opcionalmente .zst junto a cada recurso, y el servidor elige el archivo correspondiente sin comprimir nada en tiempo de petición.

brotli -q 11 app.js    # writes app.js.br; source kept by default
gzip -9 -k app.js      # writes app.js.gz; -k keeps the source
zstd -19 app.js        # writes app.js.zst; source kept by default

La CLI de brotli conserva las entradas por defecto y acepta -q para una calidad de 0 a 11. GNU gzip necesita -k o --keep para dejar el original en su sitio.

Servir los archivos hermanos en nginx requiere dos directivas:

load_module modules/ngx_http_brotli_static_module.so;

http {
    gzip_static   on;   # serves .gz siblings; module needs --with-http_gzip_static_module
    brotli_static on;   # serves .br siblings; default off
    gzip_vary     on;   # adds Vary: Accept-Encoding; default off
}

ngx_http_gzip_static_module no se compila por defecto. brotli_static proviene de ngx_brotli. nginx no incluye ningún módulo zstd propio; el módulo de terceros zstd-nginx-module añade una directiva zstd_static para archivos hermanos .zst, desactivada por defecto, y sin ella zstd para archivos estáticos es un ajuste del lado de la CDN.

Las CDN dejan pasar los archivos precomprimidos. La documentación de compresión de Cloudflare indica que conserva el content-encoding: br o gzip del origen cuando el navegador del visitante lo admite y no hay activadas funciones de reescritura de respuestas (Rocket Loader, Email Address Obfuscation, Polish y otras); solicita a los orígenes con accept-encoding: br, gzip, así que el zstd del origen no se deja pasar. El comportamiento Brotli Support de Akamai sirve y cachea Brotli comprimido en origen, devuelve variantes no Brotli a los clientes que no aceptan br, y no realiza compresión en el edge por sí mismo.

Veredicto: comprime en tiempo de build, al nivel máximo, y configura el servidor o la CDN para servir el archivo hermano.

¿Cómo acuerdan el navegador y el servidor una codificación?

El navegador enumera las codificaciones que puede decodificar en Accept-Encoding, el servidor o la CDN elige una y etiqueta la respuesta con Content-Encoding, y ningún código de aplicación participa en el intercambio.

GET /app.3f2a1b.js HTTP/1.1
Accept-Encoding: gzip, deflate, br, zstd
HTTP/1.1 200 OK
Content-Encoding: br
Vary: Accept-Encoding

br es el token de Brotli en el registro de content-coding de HTTP, definido en la sección 13 del RFC 7932. Vary: Accept-Encoding indica a las cachés compartidas que no entreguen un cuerpo Brotli a un cliente que solo pidió gzip; nginx la omite salvo que se establezca gzip_vary on.

Para ver qué sirve un sitio hoy, solicita un archivo de bundle y lee la única cabecera que llega de vuelta:

curl -sI -H 'Accept-Encoding: gzip, br, zstd' https://example.com/app.js | grep -i content-encoding

La misma información aparece en el panel Network de DevTools, en las cabeceras de respuesta.

El soporte de los navegadores determina el orden de fallback. Brotli está soportado por todos los navegadores principales actuales. caniuse lista zstd en Chrome y Edge desde la versión 123, Firefox desde la 126, Opera desde la 109 y Safari desde la 26, con Safari de escritorio marcado como soporte parcial. A cualquier navegador que omita zstd de Accept-Encoding simplemente se le sirve Brotli o gzip. No hay modo de fallo, solo un fallback.

Veredicto: cambiar de algoritmo es un cambio de configuración, y la cadena de fallback lo hace seguro.

¿Qué no deberías comprimir?

Recomprimir JPEG, MP4 o WOFF2 desperdicia CPU en ambos extremos para un ahorro prácticamente nulo, porque esos formatos ya están comprimidos internamente. WOFF2 es el caso más claro: la sección 1.2 del RFC 7932 recoge que el formato que define está integrado en WOFF 2.0, de modo que un archivo .woff2 ya es salida de Brotli.

Las respuestas muy pequeñas son la otra exclusión. Por debajo de unas pocas decenas de bytes, la sobrecarga de la codificación supera el ahorro, razón por la cual gzip_min_length de nginx y brotli_min_length de ngx_brotli tienen un valor por defecto de 20 bytes, y Cloudflare solo comprime respuestas de al menos 48 bytes para gzip y 50 para Brotli y zstd.

Las conclusiones aquí se aplican a los archivos que los navegadores reciben realmente, de unos pocos kilobytes a unos pocos megabytes. Los benchmarks que muestran a Brotli tardando minutos provienen de archivos de cientos de megabytes que nunca viajan a través de Content-Encoding.

Veredicto: comprime texto (HTML, CSS, JavaScript, JSON, SVG); omite medios, fuentes y respuestas diminutas.

Conclusión

La cuestión del algoritmo tiene una respuesta asentada: Brotli al nivel máximo para todo lo que se comprime en tiempo de build, zstd para todo lo que se comprime por petición, gzip como suelo que todos los clientes entienden. La cuestión del nivel es la que la mayoría de los stacks nunca se ha planteado. Ejecuta la comprobación con curl contra tu propio bundle, anota la codificación e infiere el nivel a partir del tamaño; y si la respuesta es gzip con un valor rápido por defecto, precomprimir con Brotli está a un paso de build y dos directivas de servidor de distancia.

Preguntas frecuentes

¿Por qué mi sitio sigue enviando gzip cuando el navegador soporta Brotli?

Tres causas explican casi todos los casos. Primera, Chrome y Firefox solo anuncian 'br' en Accept-Encoding sobre HTTPS, así que las peticiones por HTTP plano, incluida la mayoría del desarrollo en localhost, recurren a gzip. Segunda, el servidor no tiene cargado ningún módulo Brotli; nginx necesita ngx_brotli, que no viene incorporado. Tercera, una CDN como Cloudflare deja pasar una respuesta de origen ya etiquetada como content-encoding: gzip en lugar de transcodificarla a Brotli.

¿Cuál es la diferencia entre 'deflate' y 'gzip' en Content-Encoding?

Ambos llevan el mismo algoritmo DEFLATE del RFC 1951 y difieren solo en el envoltorio. En HTTP, 'deflate' significa el formato zlib del RFC 1950 (cabecera de dos bytes, suma de verificación Adler-32) y 'gzip' significa el contenedor del RFC 1952 con un cierre CRC-32. Los primeros servidores y navegadores enviaban a veces DEFLATE en bruto bajo el nombre 'deflate', obligando a los clientes a adivinar, de modo que gzip se convirtió en la opción fiable y deflate rara vez merece la pena ofrecerlo.

¿Node.js soporta compresión Brotli y zstd de forma nativa?

Sí. El módulo integrado node:zlib implementa las codificaciones de contenido gzip, deflate, br y zstd sin paquetes de terceros. Brotli está disponible desde Node.js 11.7.0 mediante zlib.brotliCompress y zlib.createBrotliCompress; Zstandard llegó en Node.js 23.8.0 con zlib.zstdCompress y zlib.createZstdCompress, y Node.js 24.6.0 añadió soporte de diccionarios a las APIs de zstd (https://nodejs.org/en/blog/release/v24.6.0). El middleware de compresión anterior a estas versiones puede seguir negociando solo gzip, así que comprueba el content-encoding que emite.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.