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
- 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,documentyundefined— aparecen en casi todo bundle de JavaScript. Gzip no tiene este diccionario, así que tiene que codificar esos bytes desde cero. - Negociación de contenido — El navegador envía
Accept-Encoding: br, gzipy Nginx elige el primer formato que soporte. Acá está el flujo completo:
- Compresión estática vs dinámica —
brotli_staticsirve archivos.brpre-generados sin costo en runtime.brotli oncomprime 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. - 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 11en 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ó:
| Algoritmo | Nivel | Tamaño transfer | Tiempo compresión | Ahorro vs Gzip |
|---|---|---|---|---|
| Gzip | 6 | 113 KB | 12 ms | baseline |
| Brotli | 4 | 108 KB | 8 ms | 4.4% |
| Brotli | 6 | 102 KB | 14 ms | 9.7% |
| Brotli | 11 | 96 KB | 180 ms | 15.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/woff2eimage/svg+xmlabrotli_types— comprimen bien. - No incluyas formatos ya comprimidos como JPEG, PNG, WebP o MP4.
- Habilitá
brotli_vary onpara que los caches manejen correctamente las variantes de codificación. - Testeá con Lighthouse o
curldespués de cada cambio de configuración.
Errores Comunes
- Olvidar instalar o cargar
ngx_brotliy pensar quebrotli onfunciona 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-Typeincorrecto. - No agregar
bra la cache key del CDN, lo que genera respuestas con codificación mezclada.
Ver También
- Módulo Nginx Brotli (ngx_brotli) — repo oficial de GitHub
- Documentación de Google Brotli — referencia del algoritmo de compresión
- Cloudflare: compresión Brotli — resultados reales en CDN
- web.dev: Reducir payloads de JavaScript — compresión como parte de optimización de bundles
- Mozilla: Content-Encoding — referencia del header HTTP
- Compresión con Gzip — setup de Gzip y estrategia de fallback
- CDN Edge Caching — cachear variantes comprimidas en el edge
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.
Recursos Relacionados
Comprimir y Descomprimir Archivos con Gzip y Brotli
Cómo reducir tamaños de archivos para APIs, assets estáticos y logs usando Gzip, Brotli y zlib con compresión streaming, negociación de contenido y lo que funciona.
RecipeImplementar CDN edge caching
Configura redes de entrega de contenido con reglas de edge caching, invalidación de caché y optimización geográfica para contenido estático y en vivo.
RecipeOptimización de Performance Web
Mejora Core Web Vitals, reduce tamaños de bundle y optimiza performance frontend con lazy loading, code splitting y herramientas de build modernas.
RecipeImplementar Estrategias de Invalidación de Caché
Cómo mantener la caché consistente con las bases de datos usando TTL, write-through, write-behind y patrones de invalidación event-driven.
RecipeImplementar Lazy Loading para Imágenes, Componentes y Datos
Cómo diferir la carga de recursos no críticos hasta que sean necesarios, mejorando el tiempo de carga inicial de página, reduciendo el ancho de banda y optimizando Core Web Vitals.
GuideGuía de Optimización de Performance Web
manual detallado para optimizar el rendimiento de aplicaciones web con mejores Core Web Vitals y experiencia de usuario.