Um stream é a resposta do Node para uma pergunta que todo programa com uso intensivo de I/O eventualmente enfrenta: o que fazer quando os dados são muito grandes, chegam muito lentamente ou são muito abertos para serem razoavelmente mantidos na memória de uma vez? Em vez de retornar um valor em um único passo, um stream entrega dados como uma sequência de pedaços menores ao longo do tempo, chegando à medida que ficam disponíveis.
Conceitos Básicos de Streams mostra código funcional para os quatro tipos de stream, e Backpressure, Padrões Readable & Writable, e Transform & Duplex aprofundam cada peça do quadro. Esta página é a moldura para todos eles: por que os streams existem, como as peças cooperam e o mecanismo de feedback que torna todo o sistema seguro sob carga.
Um stream é uma abstração sobre dados que chegam em chunks ao longo do tempo, permitindo que o código processe entrada de tamanho arbitrário ou lenta com uso de memória limitado e previsível.
Por que Importa: Carregar um arquivo inteiro, corpo de resposta ou upload na memória antes de processá-lo não escala - streams permitem que um serviço lide com cargas úteis muito maiores do que a RAM disponível, e comece a produzir saída antes mesmo que a entrada termine de chegar.
Quando Usar: Ler ou escrever arquivos grandes, fazer proxy de corpos de requisição/resposta HTTP, transformar dados em trânsito (compressão, parsing, criptografia), ou conectar dois endpoints de I/O sem armazenar tudo em buffer entre eles.
Limitações / Trade-offs: Streams trocam simplicidade por eficiência - tratamento de erros, limpeza e backpressure exigem tratamento deliberado que uma única chamada await readFile() nunca pede a você.
Tópicos Relacionados: Buffers, o event loop do Node.js, stream/promises pipeline, streaming de respostas HTTP.
Antes dos streams, a maneira óbvia de lidar com um arquivo ou payload de rede no Node era ler tudo na memória, e então operar sobre o resultado completo - simples de raciocinar, mas viável apenas se os dados forem pequenos e finitos.
As APIs principais do Node (http, fs, net, zlib, e mais) são construídas em torno de uma ideia diferente: expor dados como uma sequência de chunks discretos, cada um um Buffer, string, ou (em modo objeto) um valor JavaScript arbitrário, entregue à medida que ficam prontos em vez de todos de uma vez. Um stream é o objeto que gerencia essa sequência - produzindo chunks (um Readable), consumindo-os (um Writable), ou ambos.
Uma analogia útil: pense em um stream como uma esteira transportadora entre duas estações de trabalho em vez de uma única caixa entregue por empilhadeira. A esteira move itens um de cada vez, a estação receptora pode trabalhar em cada item à medida que ele chega em vez de esperar por todo o carregamento, e - criticamente - a esteira pode ser instruída a desacelerar ou parar se a estação receptora ficar para trás, em vez de empilhar caixas no chão.
O Node define quatro tipos de stream, cada um um papel nessa esteira:
Todo tipo de stream é construído sobre EventEmitter - um Readable emite 'data' e 'end', um Writable emite 'drain' e 'finish', e todo stream pode emitir 'error'. Essa base compartilhada é por que os streams se compõem da maneira que fazem: conectar streams é realmente conectar produtores de eventos a consumidores de eventos, com as classes de stream gerenciando a contabilidade (buffers internos, máquinas de estado) por cima.
A peça mais importante dessa contabilidade é a backpressure. Todo Writable tem um buffer interno com um highWaterMark configurável (um limite suave de bytes ou contagem de objetos); quando um produtor escreve mais rápido do que um consumidor pode drenar esse buffer, writable.write() começa a retornar false como um sinal para pausar. Uma API de stream que ignora esse sinal e continua escrevendo, derrota todo o propósito de limitação de memória de usar um stream em primeiro lugar - o buffer interno apenas cresce sem limite. Backpressure cobre o evento drain e as mecânicas manuais de pause/resume completamente.
pipe() (e sua contraparte moderna mais segura, stream/promises' pipeline()) existe especificamente para que você raramente precise gerenciar esse loop de feedback manualmente: fazer o pipe de um Readable para um Writable conecta o tratamento de backpressure, encaminha chunks de 'data', e (no caso de pipeline()) propaga erros e garante a limpeza se um dos lados falhar.
import { pipeline } from 'node:stream/promises';import { createReadStream, createWriteStream } from 'node:fs';import { createGzip } from 'node:zlib';// Cada estágio segura apenas uma janela limitada de dados na memória -// não o arquivo inteiro - e a backpressure do estágio de escrita// automaticamente desacelera o estágio de leitura se o I/O de disco ficar para trás.await pipeline( createReadStream('input.log'), createGzip(), createWriteStream('input.log.gz'),);
Esse trecho também demonstra por que os streams Transform importam como sua própria categoria: createGzip() é simultaneamente um Writable (aceitando bytes brutos) e um Readable (emitindo bytes comprimidos), com sua saída sendo derivada causalmente de sua entrada - uma forma suficientemente distinta de um Duplex geral (cujos lados de leitura e escrita são independentes, como as duas direções de um socket TCP) que o Node o modela como sua própria subclasse. Transform & Duplex cobre a construção de versões personalizadas de ambos.
Streams interagem diretamente com o modelo de I/O do event loop: um Readable originado de um arquivo ou socket não faz polling por dados, ele confia no mecanismo de conclusão de I/O subjacente do libuv para entregar chunks à medida que o SO os torna disponíveis, o que é parte do motivo pelo qual o I/O em streaming escala bem sob o design de thread única e não bloqueante do Node, em vez de lutar contra ele.
O tratamento de erros é a aresta operacional mais afiada em todo esse modelo. Como um pipeline é realmente vários EventEmitters que emitem independentemente e são conectados, um 'error' não tratado em qualquer um deles trava o processo por padrão - e uma cadeia pipe() ingênua não destrói automaticamente todos os outros streams na cadeia quando um elo falha, o que pode vazar descritores de arquivo ou sockets abertos. stream/promises' pipeline() foi construído especificamente para fechar essa lacuna: ele destrói todos os streams na cadeia em qualquer falha e apresenta uma única Promise rejeitada em vez de eventos dispersos para escutar. stream/promises Pipeline cobre isso em profundidade, e Melhores Práticas de Streams transforma isso em regras concretas.
HTTP é um dos lugares de maior alavancagem onde este modelo aparece na prática: tanto a requisição de entrada quanto a resposta de saída no módulo http do Node são streams, o que significa que um serviço proxy ou adjacente a um proxy reverso pode encaminhar um corpo de requisição ou resposta grande sem nunca armazenar tudo em buffer - Streaming de Respostas HTTP cobre os detalhes de timing de cabeçalho e codificação de transferência em chunks que vêm com isso.
Abordagem
Força
Fraqueza
Melhor Ajuste
Buffer de carga útil inteira (readFile, middleware req.body)
Código simples, com sensação síncrona; fácil de raciocinar
Memória escala com o tamanho da carga útil; sem progresso até carregar completamente
Cargas úteis pequenas e limitadas (arquivos de configuração, corpos JSON pequenos)
Cadeias pipe() manuais
Sem dependência extra; controle direto
Tratamento de erros e limpeza em toda a cadeia são responsabilidade do chamador
Encaminhamento simples de estágio único com listeners de erro cuidadosos
stream/promisespipeline()
Limpeza automática e propagação de erros em todos os estágios
Um pouco mais de cerimônia do que uma chamada pipe() nua
Qualquer pipeline multi-estágio ou de produção
Iteração Assíncrona (for await...of um Readable)
Lê naturalmente como código sequencial; integra-se com async/await
Cobre apenas o lado do consumidor; ainda precisa de cuidado ao compor múltiplos estágios
"Streams são apenas uma maneira mais lenta de obter os mesmos dados que readFile." Eles não são sobre velocidade para uma única leitura - são sobre limitar a memória e permitir que o processamento comece antes que a entrada esteja totalmente disponível, o que importa mais exatamente quando as cargas úteis são grandes ou abertas.
"pipe() lida com erros para você." Ele encaminha dados e gerencia backpressure, mas um erro em um stream em uma cadeia pipe() não destrói automaticamente os outros - pipeline() foi construído para fechar essa lacuna específica.
"Backpressure é algo que você só precisa pensar para arquivos enormes." Qualquer descompasso na velocidade do produtor/consumidor o aciona - um Transform rápido em memória alimentando um Writable de rede lento atinge as mesmas mecânicas de highWaterMark que uma cópia de arquivo de vários gigabytes.
"Streams em modo objeto são um recurso de nicho." Eles são o mecanismo por trás de padrões comuns como fazer o pipe de linhas de banco de dados analisadas ou registros JSON delimitados por nova linha através de um pipeline de processamento - o modo objeto apenas significa que o stream carrega valores JS inteiros em vez de bytes.
"Um stream Transform é basicamente um stream Duplex com um nome diferente." Um Duplex tem dois lados independentes - o que você escreve e o que você lê não estão relacionados (um socket TCP, por exemplo). Um Transform é um Duplex especializado onde a saída é derivada diretamente da entrada, como um compressor gzip transformando bytes brutos em bytes comprimidos.
Eles permitem que o código processe dados que são muito grandes, chegam muito lentamente ou são muito abertos para serem razoavelmente mantidos inteiramente na memória - entregando-os em chunks limitados ao longo do tempo em vez de como um valor completo.
Quais são os quatro tipos de stream, em uma linha cada?
Readable - produz uma sequência de chunks (um arquivo sendo lido, uma requisição HTTP de entrada)
Writable - consome uma sequência de chunks (um arquivo sendo escrito, uma resposta HTTP de saída)
Duplex - ambos os lados independentemente (um socket TCP)
Transform - ambos os lados, com saída derivada da entrada (compressão gzip, um parser)
Como os streams se relacionam com EventEmitter?
Cada classe de stream é construída sobre EventEmitter - Readables emitem 'data'/'end', Writables emitem 'drain'/'finish', e todos os streams podem emitir 'error'; as classes de stream adicionam lógica de buffer e máquina de estado sobre essa base de eventos compartilhada.
O que exatamente é backpressure?
É o sinal de feedback que um Writable dá quando seu buffer interno está cheio - write() retorna false, dizendo ao produtor para pausar até que um evento 'drain' diga que é seguro retomar - que é o que impede um produtor rápido de crescer a memória sem limites ao escrever para um consumidor mais lento.
Por que usar `pipeline()` em vez de `pipe()`?
pipeline() (de node:stream/promises) destrói todos os streams em uma cadeia multi-estágio se qualquer um deles der erro, e resolve ou rejeita uma única Promise para toda a operação - uma cadeia pipe() nua não propaga erros ou limpa outros estágios automaticamente, o que pode vazar handles abertos.
Streams só funcionam com dados binários?
Não - o modo objeto permite que um stream carregue valores JavaScript arbitrários em vez de bytes ou strings, que é como padrões como streaming de linhas analisadas ou registros JSON através de um pipeline de processamento funcionam; highWaterMark então conta objetos em vez de bytes.
É aceitável simplesmente armazenar uma carga útil inteira em buffer em vez de fazer streaming dela?
Sim - para dados pequenos e limitados (um arquivo de configuração, um corpo JSON pequeno) armazenar tudo em buffer é mais simples e o custo de memória é insignificante; o streaming ganha sua complexidade especificamente quando o tamanho da carga útil é grande ou desconhecido com antecedência.
Como os streams interagem com o event loop?
Um stream originado de um arquivo ou socket confia no mecanismo de conclusão de I/O do libuv para entregar chunks à medida que o SO os torna prontos, em vez de fazer polling - então o I/O em streaming se encaixa no modelo não bloqueante do Node em vez de trabalhar contra ele.
Por que um erro de stream não tratado trava todo o processo?
Porque 'error' é um evento especial do EventEmitter - se nenhum listener for registrado para ele, o Node o trata como uma exceção não capturada e termina o processo por padrão, que é por que cada stream em uma cadeia precisa de tratamento de erros, não apenas o primeiro.
Qual é a diferença entre um stream Duplex e um Transform?
Um Duplex tem dois lados independentes - o que você escreve e o que você lê não estão relacionados (um socket TCP, por exemplo). Um Transform é um Duplex especializado onde a saída é derivada diretamente da entrada, como um compressor gzip transformando bytes brutos em bytes comprimidos.
Posso consumir um stream Readable com `async`/`await` em vez de eventos?
Sim - qualquer Readable é um iterável assíncrono, então for await (const chunk of readable) funciona e lê naturalmente como código sequencial, enquanto o stream ainda entrega chunks (e respeita backpressure) nos bastidores exatamente como faria com listeners de eventos brutos.