Express.js
Compression y Content-Encoding
Explica compresión HTTP, Content-Encoding, negociación, Vary, costes de CPU y cuándo comprimir responses desde Express.js o desde el proxy.
- Última actualización
- Actualizada
- Nivel
- Aplicación
Express.js
Explica compresión HTTP, Content-Encoding, negociación, Vary, costes de CPU y cuándo comprimir responses desde Express.js o desde el proxy.
La compresión reduce bytes enviados a cambio de CPU, buffering y complejidad de cache. No debe activarse por costumbre: debe aplicarse donde el ahorro supera el coste.
HTTP permite negociar una representación comprimida mediante Accept-Encoding y Content-Encoding.
cliente: Accept-Encoding: br, gzip
↓
servidor elige una codificación compatible
↓
response: Content-Encoding: brLa representación sigue siendo JSON, HTML o CSS; Content-Encoding indica una transformación aplicada durante el transporte.
Textos y JSON suelen contener patrones repetidos. Enviar una respuesta de 500 KB sin compresión consume más ancho de banda, aumenta tiempo de transferencia y puede empeorar la experiencia en redes lentas.
Sin embargo, comprimir también consume CPU y puede añadir latencia. Un archivo JPEG, WebP, ZIP o video ya comprimido suele obtener poco beneficio y puede incluso crecer.
body original
↓ serialización
bytes sin comprimir
↓ algoritmo gzip/br/deflate
bytes codificados
↓ red
cliente descomprimeLa compresión no cambia el significado del recurso, pero sí cambia sus bytes. Por eso caches y proxies deben distinguir representaciones según Accept-Encoding.
El paquete compression puede negociar automáticamente:
import compression from 'compression';
app.use(compression({
threshold: 1_024,
}));El threshold evita gastar CPU en cuerpos muy pequeños. Debe medirse según payloads y plataforma; no existe un valor universal.
Suele aportar valor en:
Suele aportar poco en:
const shouldCompress: compression.CompressionFilter = (request, response) => {
if (request.headers['x-no-compression']) {
return false;
}
const contentType = response.getHeader('Content-Type');
if (typeof contentType === 'string' && contentType.startsWith('text/event-stream')) {
return false;
}
return compression.filter(request, response);
};
app.use(compression({
threshold: 2_048,
filter: shouldCompress,
}));Flujo:
Accept-Encoding.Content-Encoding y debe variar por Accept-Encoding.En producción, un reverse proxy o CDN puede comprimir mejor que cada instancia de Node:
Express produce representación
↓
proxy/CDN aplica gzip o Brotli
↓
cachea variantes
↓
clienteVentajas:
El riesgo es comprimir dos veces o tener reglas inconsistentes. Define una sola capa responsable por tipo de contenido.
Brotli suele comprimir texto mejor, especialmente con niveles altos, pero puede costar más CPU. Gzip tiene compatibilidad amplia y velocidad predecible.
Para contenido dinámico, niveles agresivos pueden aumentar demasiado la latencia. Para assets estáticos precomprimidos durante build, niveles altos son más razonables.
La compresión puede procesar por chunks, pero mantiene buffers internos. En respuestas pequeñas, el middleware puede esperar suficiente información antes de emitir datos.
En Server-Sent Events, el buffering puede retrasar eventos. Puede ser necesario desactivar compresión o forzar flush según la implementación.
Una misma URL puede tener una representación gzip y otra sin comprimir. La cache necesita saber que Accept-Encoding afecta el resultado:
Vary: Accept-EncodingEl middleware o proxy suele configurarlo. Si se omite, una cache podría entregar bytes comprimidos a un cliente incompatible o perder eficiencia.
Un ETag puede representar los bytes codificados o la entidad antes de codificación, según la capa que lo genere. La coordinación incorrecta produce validaciones condicionales inconsistentes.
Cuando el proxy comprime después de Express, revisa cómo reescribe o debilita ETags.
Ataques como BREACH explotan diferencias de tamaño cuando una response comprimida combina:
Ejemplo conceptual:
HTML contiene token CSRF secreto
+
refleja texto enviado por atacante
+
response comprimida observableMitigaciones dependen del contexto:
Desactivar toda compresión no siempre es necesario; debe existir un threat model real.
Comprimir respuestas grandes consume CPU. Bajo alta concurrencia, el event loop y thread pool pueden saturarse.
Mide:
Un ratio excelente no compensa una caída severa de throughput.
Entrega la representación sin codificar.
Si no existe una codificación aceptable y identity fue rechazada explícitamente, el contrato podría producir 406. En la práctica, la mayoría de clientes acepta identity.
No vuelvas a comprimir. Detecta formato, headers o ruta.
No hay contenido útil para comprimir.
Puede ser mejor redirigir a una URL firmada o dejar que storage/CDN gestione codificación y range requests.
Puede gastar CPU en respuestas donde no existe ahorro.
Añade trabajo con beneficio mínimo.
Produce contenido inválido o trabajo innecesario.
Sin Vary, las variantes pueden mezclarse.
Aumenta CPU y latencia por una reducción marginal adicional.
Un test puede solicitar gzip:
await request(app)
.get('/large-json')
.set('Accept-Encoding', 'gzip')
.expect('Content-Encoding', /gzip/)
.expect(200);Además, prueba:
Accept-Encoding negocia y Content-Encoding describe.Vary: Accept-Encoding?Webhooks: firma, idempotencia y retries aplica parsing raw, autenticación criptográfica y procesamiento resiliente a eventos externos.