O CompressionStream no Node.js permite comprimir dados usando a interface de Web Streams. Em vez de configurar transformações específicas do módulo zlib, a aplicação cria um CompressionStream e conecta o fluxo com pipeThrough(), seguindo o mesmo modelo usado por navegadores modernos.
A API é útil para ferramentas multiplataforma, processamento de respostas, geração de arquivos, integração com fetch e bibliotecas que já trabalham com ReadableStream. Ela também facilita decomprimir conteúdo com DecompressionStream. Mesmo assim, compressão continua envolvendo CPU, memória, limites de saída e riscos como decompression bombs.
Neste guia, você aprenderá a usar gzip, deflate e Brotli quando disponíveis, converter texto e buffers, integrar Web Streams com streams clássicas, aplicar backpressure, comprimir respostas HTTP, testar round trips e escolher quando preferir node:zlib.
Disponibilidade
CompressionStream está disponível globalmente em versões modernas do Node.js e segue o formato da Compression Streams API. Consulte a documentação oficial do Node.js para confirmar estabilidade e formatos suportados pela versão usada.
if (typeof CompressionStream === 'undefined') {
throw new Error('CompressionStream não está disponível');
}Versões recentes aceitam formatos tradicionais e podem incluir Brotli. Faça feature detection e testes no runtime real, porque o suporte varia por versão.
Primeiro exemplo com gzip
const input = new TextEncoder().encode('Olá, mundo!');
const source = new Blob([input]).stream();
const compressed = source.pipeThrough(
new CompressionStream('gzip')
);
const result = new Uint8Array(
await new Response(compressed).arrayBuffer()
);
console.log(result.byteLength);O fluxo de entrada passa pelo transformador e a resposta consome o stream comprimido. Para arquivos grandes, não reúna toda a saída em memória; envie ou grave o fluxo diretamente.
Formatos
Os formatos mais comuns são:
gzip: amplamente usado em HTTP e arquivos.gz;deflate: formato zlib;deflate-raw: fluxo DEFLATE sem cabeçalho zlib, quando suportado;brotli: compressão eficiente para texto, disponível apenas em versões compatíveis.
const compressor = new CompressionStream('gzip');Não aceite o nome do formato diretamente de entrada não confiável. Use uma allowlist.
const allowed = new Set(['gzip', 'deflate']);
function createCompressor(format) {
if (!allowed.has(format)) {
throw new Error('Formato não permitido');
}
return new CompressionStream(format);
}Decompressão
const decompressed = compressedStream.pipeThrough(
new DecompressionStream('gzip')
);
const text = await new Response(decompressed).text();O formato da descompressão deve corresponder ao formato usado na compressão. Um valor incorreto causa erro durante o consumo do stream.
Round trip
async function roundTrip(text) {
const source = new Blob([text]).stream();
const compressed = source.pipeThrough(
new CompressionStream('gzip')
);
const decompressed = compressed.pipeThrough(
new DecompressionStream('gzip')
);
return new Response(decompressed).text();
}
console.log(await roundTrip('conteúdo de teste'));Esse tipo de teste confirma que o pipeline preserva os dados.
Web Streams
CompressionStream implementa a mesma forma de um TransformStream, com propriedades readable e writable. Isso permite escrever manualmente:
const compression = new CompressionStream('gzip');
const writer = compression.writable.getWriter();
await writer.write(new TextEncoder().encode('parte 1'));
await writer.write(new TextEncoder().encode('parte 2'));
await writer.close();
const output = compression.readable;Na maioria dos casos, pipeThrough() é mais simples e preserva backpressure automaticamente.
Veja Web Streams API no Node.js.
Backpressure
Quando o destino consome lentamente, Web Streams sinalizam ao produtor para reduzir o ritmo. Não ignore as Promises de escrita:
await writer.write(chunk);Acumular chunks sem aguardar pode elevar o consumo de memória e derrotar o controle de fluxo.
Integrando com fetch
const source = new Blob([JSON.stringify(payload)]).stream();
const body = source.pipeThrough(new CompressionStream('gzip'));
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'content-type': 'application/json',
'content-encoding': 'gzip'
},
body,
duplex: 'half'
});Confirme se o servidor aceita corpo comprimido. O cabeçalho Content-Encoding descreve a transformação aplicada ao conteúdo.
Consulte Fetch no Node.js.
Respostas HTTP comprimidas
Ao servir conteúdo, avalie o cabeçalho Accept-Encoding e responda apenas com formato suportado pelo cliente.
function chooseEncoding(value = '') {
if (value.includes('br')) return 'br';
if (value.includes('gzip')) return 'gzip';
return null;
}Uma negociação completa deve considerar pesos q, wildcards e exclusões. Para aplicações de produção, use biblioteca ou framework que implemente o protocolo corretamente.
Ao variar por encoding:
res.setHeader('Vary', 'Accept-Encoding');Sem Vary, caches podem entregar uma resposta Brotli a um cliente que não suporta o formato.
Veja Compressão HTTP no Node.js.
Não comprima tudo
Evite compressão quando:
- o corpo é muito pequeno;
- o conteúdo já está comprimido, como JPEG, PNG, ZIP ou MP4;
- a CPU é o gargalo;
- a resposta é extremamente sensível à latência inicial;
- a compressão aumenta o tamanho;
- o cliente não anunciou suporte.
Defina um tamanho mínimo e uma lista de MIME types compressíveis.
function shouldCompress({ contentType, size }) {
if (size < 1024) return false;
return contentType.startsWith('text/') ||
contentType.includes('json') ||
contentType.includes('javascript');
}Streaming de arquivo
Para arquivos grandes, converta uma stream clássica para Web Stream:
import { createReadStream } from 'node:fs';
import { Readable } from 'node:stream';
const file = createReadStream('report.json');
const webStream = Readable.toWeb(file);
const compressed = webStream.pipeThrough(
new CompressionStream('gzip')
);Para voltar ao modelo clássico:
const nodeStream = Readable.fromWeb(compressed);Isso permite conectar a arquivos, sockets e APIs tradicionais.
Consulte Streams no Node.js.
Gravando um arquivo gzip
import { createReadStream, createWriteStream } from 'node:fs';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
const source = Readable.toWeb(
createReadStream('data.ndjson')
);
const compressed = source.pipeThrough(
new CompressionStream('gzip')
);
await pipeline(
Readable.fromWeb(compressed),
createWriteStream('data.ndjson.gz')
);pipeline() propaga erros e fecha recursos corretamente.
Brotli
Brotli costuma produzir arquivos menores para texto, mas pode consumir mais CPU. Verifique suporte:
function supportsBrotli() {
try {
new CompressionStream('brotli');
return true;
} catch {
return false;
}
}Meça tempo de compressão, tamanho final e efeito na latência. Para conteúdo estático, comprimir durante o build pode ser melhor que comprimir em cada requisição.
CPU-bound
Compressão consome CPU. Em alto volume, ela pode bloquear trabalho útil ou aumentar o event loop lag. Considere:
- limitar concorrência;
- pré-comprimir assets;
- usar CDN ou proxy;
- mover tarefas pesadas para Worker Threads;
- reduzir nível de compressão usando
node:zlibquando precisar de controle fino.
Veja Worker Threads no Node.js.
CompressionStream ou zlib?
Use CompressionStream quando:
- a aplicação já trabalha com Web Streams;
- portabilidade com navegador é importante;
- a configuração padrão é suficiente;
- você quer uma API simples e padronizada.
Use node:zlib quando:
- precisa ajustar nível, janela ou estratégia;
- usa streams clássicas em todo o projeto;
- precisa de APIs específicas;
- quer controle operacional detalhado.
Decompression bombs
Um arquivo pequeno pode expandir para gigabytes. Nunca descomprima conteúdo não confiável sem limites.
async function readWithLimit(stream, maxBytes) {
const reader = stream.getReader();
const chunks = [];
let total = 0;
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
total += value.byteLength;
if (total > maxBytes) {
throw new Error('Saída descomprimida excedeu o limite');
}
chunks.push(value);
}
} finally {
reader.releaseLock();
}
return chunks;
}Também aplique timeout, limite de entrada e cancelamento.
Cancelamento
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5_000);
try {
const response = await fetch(url, {
signal: controller.signal
});
} finally {
clearTimeout(timeout);
}Propague cancelamento ao pipeline quando o consumidor abandonar a operação.
Erros de formato
DecompressionStream lança erro quando o conteúdo está truncado ou incompatível. Capture a falha e não devolva detalhes internos ao cliente.
try {
const output = input.pipeThrough(
new DecompressionStream('gzip')
);
await new Response(output).arrayBuffer();
} catch {
throw new Error('Conteúdo comprimido inválido');
}Segurança e BREACH
Comprimir respostas que misturam segredos e entrada controlada pelo atacante pode permitir ataques de análise de tamanho. Tokens, CSRF secrets e dados altamente sensíveis não devem ser refletidos em respostas comprimidas junto com entrada do usuário sem avaliação de risco.
Content-Length
Quando a saída é produzida em streaming, o tamanho final normalmente não é conhecido antecipadamente. Não envie um Content-Length calculado sobre o conteúdo original. Use transferência apropriada ou pré-comprima o arquivo.
Testes
import test from 'node:test';
import assert from 'node:assert/strict';
test('comprime e descomprime gzip', async () => {
const original = 'abc'.repeat(10_000);
const source = new Blob([original]).stream();
const compressed = source.pipeThrough(
new CompressionStream('gzip')
);
const restored = compressed.pipeThrough(
new DecompressionStream('gzip')
);
assert.equal(await new Response(restored).text(), original);
});Teste dados vazios, Unicode, chunks pequenos, conteúdo truncado, cancelamento e limite excedido.
Benchmark
Meça:
- tamanho original;
- tamanho comprimido;
- tempo de CPU;
- latência total;
- memória;
- event loop lag;
- throughput;
- efeito de concorrência.
Não escolha Brotli ou gzip apenas pela taxa de compressão. O melhor formato depende de custo computacional e comportamento do cliente.
Erros comuns
- não verificar suporte do runtime;
- aceitar formato arbitrário;
- comprimir arquivos já compactados;
- esquecer Vary;
- enviar Content-Length incorreto;
- reunir arquivo grande em memória;
- ignorar backpressure;
- descomprimir sem limite;
- usar compressão em respostas com segredos refletidos;
- não medir CPU.
Checklist
- runtime compatível;
- formatos em allowlist;
- MIME type avaliado;
- tamanho mínimo definido;
- backpressure preservada;
- Vary configurado;
- limite de saída;
- timeout e cancelamento;
- round trip testado;
- benchmark realizado.
Conclusão
O CompressionStream no Node.js leva compressão para o modelo de Web Streams. A API é direta, interoperável e funciona bem em pipelines com fetch, Blob, arquivos e respostas HTTP.
Use compressão seletivamente, preserve backpressure e limite a descompressão de conteúdo não confiável. Quando precisar de configuração avançada, combine ou substitua a API por node:zlib. Com testes e métricas, a compressão reduz tráfego sem transformar CPU e memória em novos gargalos.
Consulte também a referência de CompressionStream no MDN.



