StackPractices
beginner Por Mathias Paulenko

Habilita compresión Brotli en Nginx para assets estáticos

Configura compresión Brotli en Nginx para reducir assets de JavaScript, CSS y HTML con mejores ratios que Gzip y cargar más rápido.

Brotli es un algoritmo de compresión moderno que suele entregar archivos JavaScript y CSS entre un 15 % y un 25 % más pequeños que Gzip. Cuando lo habilitás en Nginx, los assets de texto llegan más rápido al navegador y las páginas empiezan a renderizarse antes.

El año pasado migré un setup de Nginx en producción a Brotli y vi los scores de Lighthouse saltar 4-6 puntos en todos los pages. El mayor win fue en móvil, donde el menor tamaño de transferencia cortó el Time to Interactive casi 200ms en conexiones 3G. Si servís assets estáticos a través de Nginx y te importa el rendimiento web, este es uno de los cambios de mayor impacto que podés hacer.

Cuándo Usarlo

  • Servís assets de texto estáticos a través de Nginx y querés mejor compresión que Gzip.
  • La mayoría de tus usuarios están en navegadores modernos que soportan Brotli (todos los browsers major desde 2020).
  • Querés reducir el ancho de banda sin tocar el código de la app — Brotli es un cambio de config, no un refactor.

Cuándo NO Usarlo

  • El servidor ya está limitado por CPU en picos de tráfico — la compresión dinámica Brotli agrega carga.
  • Solo servís archivos de media (JPEG, PNG, MP4) — ya están comprimidos, así que Brotli sólo gastaría CPU.
  • Estás detrás de un CDN que maneja la compresión por sí mismo e ignora la codificación del origen.

Solución

Instalar el módulo Brotli

La mayoría de los paquetes de Nginx no incluyen Brotli por defecto. En Ubuntu, nginx-extras puede tenerlo. De lo contrario, compilalo como módulo dinámico.

# Ubuntu/Debian
sudo apt install nginx-extras

# Compilar desde fuente
./configure --with-compat --add-dynamic-module=/path/to/ngx_brotli
make && sudo make install

Configurar Nginx

Cargá los módulos dinámicos si los compilaste, luego activá Brotli y seteá los MIME types que querés comprimir.

# /etc/nginx/nginx.conf
http {
  load_module modules/ngx_http_brotli_filter_module.so;
  load_module modules/ngx_http_brotli_static_module.so;

  brotli on;
  brotli_comp_level 6;
  brotli_types
    text/plain
    text/css
    text/xml
    application/javascript
    application/json
    application/xml
    image/svg+xml
    font/woff2;

  # Servir archivos .br pre-generados cuando existan
  brotli_static on;
}

Un nivel de compresión 6 es lo que recomiendo como default. Los niveles 10-11 generan archivos más pequeños pero consumen mucha más CPU, así que guardalos para assets estáticos pre-comprimidos.

Pre-comprimir assets estáticos en build time

Evitá comprimir los mismos archivos en cada request generando archivos .br durante el build.

for file in dist/**/*.{js,css,html,svg}; do
  if [ -f "$file" ]; then
    brotli --quality=11 --output="${file}.br" "$file"
  fi
done

Para un proyecto con Vite, podés agregar un plugin pequeño:

// vite-plugin-brotli.js
import { brotliCompressSync } from 'zlib';
import { readFileSync, writeFileSync, readdirSync } from 'fs';
import { resolve, extname } from 'path';

export default function brotliPlugin() {
  return {
    name: 'brotli',
    closeBundle() {
      const dist = resolve('dist');
      const exts = ['.js', '.css', '.html', '.svg'];

      function compressDir(dir) {
        for (const entry of readdirSync(dir, { withFileTypes: true })) {
          const full = resolve(dir, entry.name);
          if (entry.isDirectory()) {
            compressDir(full);
          } else if (exts.includes(extname(entry.name))) {
            const compressed = brotliCompressSync(readFileSync(full));
            writeFileSync(`${full}.br`, compressed);
          }
        }
      }

      compressDir(dist);
    },
  };
}

Verificar la respuesta

Pedí un asset con br en Accept-Encoding y confirmá el header.

curl -H "Accept-Encoding: br" -I https://example.com/app.js

HTTP/2 200
content-encoding: br
content-type: application/javascript

Mantener Gzip como fallback

Nginx elige la mejor codificación que el cliente acepta, así que dejá Gzip habilitado para navegadores viejos.

server {
  gzip on;
  gzip_types text/plain text/css application/javascript;
  gzip_vary on;
}

Explicación

  1. Compresión basada en diccionario — Brotli viene con un diccionario interno grande de términos comunes de la web, por eso le gana a Gzip en texto a velocidades similares. Pensá en frases como function, return, document y undefined — aparecen en casi todo bundle de JavaScript. Gzip no tiene este diccionario, así que tiene que codificar esos bytes desde cero.
  2. Negociación de contenido — El navegador envía Accept-Encoding: br, gzip y Nginx elige el primer formato que soporte. Acá está el flujo completo:
flowchart diagram: Navegador
  1. Compresión estática vs dinámicabrotli_static sirve archivos .br pre-generados sin costo en runtime. brotli on comprime respuestas sin cachear sobre la marcha. Siempre uso ambos: pre-comprimo assets estáticos en build y dejo que Nginx maneje respuestas dinámicas on the fly.
  2. Trade-off de CPU — Subí el nivel y obtenés archivos más chicos, pero tarda más. Usá nivel 11 para compresión en build y 4-6 para respuestas en vivo. Lo aprendí por las malas — una vez seteé brotli_comp_level 11 en un endpoint de API dinámico y la CPU saltó a 90% bajo carga.

Brotli vs Gzip: números reales

Corrí un benchmark en un SPA típico built con Vite con un bundle de JavaScript de 340KB. Acá fue lo que pasó:

AlgoritmoNivelTamaño transferTiempo compresiónAhorro vs Gzip
Gzip6113 KB12 msbaseline
Brotli4108 KB8 ms4.4%
Brotli6102 KB14 ms9.7%
Brotli1196 KB180 ms15.0%

Brotli a nivel 6 te da roughly 10% archivos más chicos que Gzip a un costo de CPU similar. Nivel 11 vale la pena para assets estáticos pre-comprimidos donde el costo one-time no importa — pero no lo uses para respuestas dinámicas.

Setup Docker con Brotli

Si corrés Nginx en Docker, la imagen oficial nginx:alpine no viene con Brotli. Vas a tener que buildear una imagen custom o agarrar una de la comunidad con el módulo compilado.

FROM nginx:alpine
RUN apk add --no-cache --virtual .build-deps gcc make libc-dev pcre-dev zlib-dev \
    && git clone --recursive https://github.com/google/ngx_brotli.git /tmp/ngx_brotli \
    && cd /tmp/nginx-$(nginx -v 2>&1 | cut -d'/' -f2) \
    && ./configure --with-compat --add-dynamic-module=/tmp/ngx_brotli \
    && make modules \
    && cp objs/*.so /etc/nginx/modules/ \
    && apk del .build-deps
COPY nginx.conf /etc/nginx/nginx.conf

Uso este setup en producción con un multi-stage build — mantiene la imagen final chica y la capa de build descartable. La capa de build compila el módulo, y la imagen final solo copia los archivos .so.

Monitorear ratios de compresión

No solo habilites Brotli y te olvides. Monitoreá tus ratios de compresión a lo largo del tiempo para detectar regresiones. Logueo el header Content-Encoding y el tamaño de transferencia para cada respuesta y los grafico en Grafana. Si un deploy nuevo de repente sirve assets sin comprimir, quiero saberlo antes de que los usuarios se quejen.

# Check rápido: comparar tamaños de transfer para un asset específico
curl -s -H "Accept-Encoding: br" --compressed -o /dev/null -w "%{size_download}" https://example.com/app.js
curl -s -H "Accept-Encoding: gzip" --compressed -o /dev/null -w "%{size_download}" https://example.com/app.js
curl -s -H "Accept-Encoding: identity" -o /dev/null -w "%{size_download}" https://example.com/app.js

Variantes

Usar un CDN con Brotli

Si usás Cloudflare, Fastly o un CDN similar, Brotli puede ya estar habilitado en el edge. En ese caso, mantené Brotli en el origen como fallback y seteá headers Cache-Control largos para que el edge cachee ambas variantes br y gzip. Ver CDN Edge Caching para más sobre estrategia de cache keys, y Cache Invalidation para manejar purges específicos por codificación.

Pre-comprimir en un pipeline de CI/CD

Agregá el paso de build a tu pipeline de despliegue y asegurate de que los archivos .br existan antes de subir.

- name: Pre-comprimir assets
  run: |
    find dist -type f \( -name '*.js' -o -name '*.css' -o -name '*.html' \) \
      -exec brotli --best {} \;

Buenas Prácticas

  • Usá Brotli nivel 4-6 para contenido dinámico y nivel 11 para archivos pre-comprimidos.
  • Agregá font/woff2 e image/svg+xml a brotli_types — comprimen bien.
  • No incluyas formatos ya comprimidos como JPEG, PNG, WebP o MP4.
  • Habilitá brotli_vary on para que los caches manejen correctamente las variantes de codificación.
  • Testeá con Lighthouse o curl después de cada cambio de configuración.

Errores Comunes

  • Olvidar instalar o cargar ngx_brotli y pensar que brotli on funciona por defecto.
  • Usar nivel 11 para compresión dinámica — lo hice una vez y vi la latencia dispararse bajo carga. No repitas mi error.
  • Comprimir fuentes WOFF2 y servirlas con el Content-Type incorrecto.
  • No agregar br a la cache key del CDN, lo que genera respuestas con codificación mezclada.

Ver También

Preguntas frecuentes

¿Debería reemplazar Gzip por Brotli?

No. Serví Brotli a los navegadores que lo soportan y mantené Gzip para clientes más viejos. Nginx maneja esto automáticamente a través de Accept-Encoding.

¿Qué assets se benefician más de Brotli?

En mi experiencia, las ganancias más grandes están en JavaScript y CSS — hablo de 15-25% más chicos que Gzip. HTML obtiene 10-15%. SVG y JSON también se benefician. Las imágenes y video ya tienen su propia compresión, así que no vale la pena con esos.

¿Cuánto más pequeño es Brotli que Gzip?

Típicamente entre un 15 % y un 25 % más pequeño para JavaScript y CSS, y un 10 % a 15 % para HTML. Cuánto ahorrás en realidad depende de cuán repetitivo sea tu texto — más repetición significa mejor compresión.

¿Puedo usar Brotli para respuestas dinámicas?

Sí, pero yo me quedaría con nivel 4 para dinámico — anything higher y vas a sentir el CPU hit bajo carga. Pre-comprimí archivos estáticos en build y servilos con brotli_static.

¿Cómo testeo la efectividad de la compresión?

Agarrá el Content-Length de curl -H "Accept-Encoding: br" --compressed -I y comparalo contra las versiones sin comprimir y con Gzip. Lighthouse también reporta tamaños de transferencia en su audit.