- O padrão Web Streams foi projetado para streaming de dados consistente entre navegador e servidor, mas hoje a complexidade e as limitações de desempenho prejudicam a experiência do desenvolvedor
- A API atual impõe carga desnecessária tanto no uso quanto na implementação por causa de restrições de design como gerenciamento de locks, BYOB e backpressure
- A Cloudflare propõe um novo modelo de streams baseado em iteração assíncrona (async iteration), e essa abordagem mostra desempenho 2x até 120x superior
- A nova API aumenta a eficiência e a consistência com uma estrutura simples de async iterable, políticas explícitas de backpressure e suporte paralelo a execução síncrona/assíncrona
- Essa abordagem pode viabilizar um modelo de streaming unificado em todos os runtimes, como Node.js, Deno, Bun e navegadores, e servir como ponto de partida para futuras discussões de padronização
Limitações estruturais do Web Streams
- O padrão WHATWG Streams foi desenvolvido entre 2014 e 2016 com foco em navegadores e, como async iteration ainda não existia na época, adotou um modelo separado de reader/writer
- Isso acabou criando etapas desnecessárias, como gerenciamento de locks, loops de leitura complexos e tratamento de buffers BYOB
- O modelo de locking monopoliza o stream e impede consumo em paralelo; se
releaseLock() for omitido, pode ocorrer o problema de o stream ficar bloqueado permanentemente
- O recurso BYOB (Bring Your Own Buffer) tinha como objetivo reutilização de memória, mas seu modelo complexo de separação e transferência de buffers reduziu o uso prático e elevou a dificuldade de implementação
- O backpressure é suportado em teoria, mas a estrutura não permite controle real, já que
enqueue() pode ter sucesso mesmo quando desiredSize é negativo
- Cada chamada a
read() força a criação de uma Promise, o que em streaming de alta frequência provoca queda de desempenho e carga extra de GC
Problemas revelados na prática
- Se o corpo de resposta de
fetch() não for consumido, ocorre esgotamento do pool de conexões; ao usar tee(), surge bufferização ilimitada em memória
TransformStream faz o processamento imediatamente, independentemente de o leitor estar pronto, causando explosão de buffer em ambientes com consumidores lentos
- Em renderização no servidor (SSR), o processamento de milhares de pequenos chunks causa GC thrashing, derrubando o desempenho
- Para mitigar isso, cada runtime (Node.js, Deno, Bun, Workers) introduziu caminhos de otimização não padronizados, o que acabou reduzindo compatibilidade e consistência
- O Web Platform Tests exige mais de 70 arquivos de teste complexos, resultado de gerenciamento excessivo de estado interno e comportamento pouco intuitivo
Princípios de design da nova API de streams
- O stream é definido como um simples async iterable, podendo ser consumido diretamente com
for await...of
- Adota transformação pull-through, processando dados apenas quando o consumidor os solicita
- Oferece políticas explícitas de backpressure (
strict, block, drop-oldest, drop-newest) para evitar explosão de memória
- Entrega dados em unidades de chunks em lote (
Uint8Array[]), reduzindo o custo de criação de Promises
- Simplifica o modelo com processamento exclusivo em nível de bytes, removendo BYOB e conceitos complexos de controller
- Inclui suporte a caminhos síncronos (synchronous) para eliminar o overhead de Promise em tarefas centradas em CPU
Exemplos e características da nova API
Stream.push() cria de forma simples um par writer/readable, e Stream.text() permite coletar o texto completo
Stream.pull() monta um pipeline lazy, executado apenas no momento do consumo
Stream.share() e Stream.broadcast() oferecem gerenciamento explícito de múltiplos consumidores
- A API paralela Sync/Async (
Stream.pullSync(), Stream.textSync()) maximiza o desempenho em operações sem I/O
- Para interoperar com Web Streams, a conversão é possível por meio de funções adaptadoras simples
Comparação de desempenho e perspectivas
- Em benchmarks com Node.js, foi confirmada velocidade até 80–90x maior; em navegadores, mais de 100x em alguns casos
- Ex.: em uma cadeia de transformação de 3 etapas, 275GB/s vs 3GB/s
- O ganho de desempenho vem da remoção do overhead assíncrono, processamento em lote e design baseado em pull
- Essa implementação foi escrita em TypeScript/JavaScript puro, com potencial de ganhos adicionais em uma implementação nativa
- A Cloudflare apresenta essa abordagem como ponto de partida para a discussão de padrões e pede feedback da comunidade de desenvolvedores
Conclusão
- O Web Streams era razoável dentro das limitações da época, mas não acompanha os recursos da linguagem e os padrões de desenvolvimento do JavaScript moderno
- O novo modelo baseado em async iterable reúne simplicidade, desempenho e controle explícito, e aponta para a possibilidade de um ecossistema de streaming consistente entre runtimes
- A Cloudflare publicou a implementação de referência, documentação e exemplos de código no GitHub em jasnell/new-streams
- O objetivo não é criar imediatamente um novo padrão, mas estabelecer um ponto de partida prático para discutir uma “API de streams melhor”
1 comentários
Comentários do Hacker News
Já projetei uma interface de Stream melhor do que a API proposta neste texto
A proposta existente tem a forma de
async iterator of UInt8Array, mas eu proponho uma estrutura em quenext()possa retornar tanto resultados síncronos quanto assíncronosCom isso,
fica possível iterar de forma mais simples com um único iterador em comparação com a estrutura atual
ao aplicar transformações síncronas a entradas síncronas, todo o processamento pode permanecer síncrono, reduzindo duplicação de código
há melhora de desempenho por reduzir a criação desnecessária de Promises
é possível fazer controle de concorrência, superando as limitações do async iterator
Com a sua abordagem não é fácil construir a estrutura deles, enquanto o contrário é possível
Iteradores centrados em I/O precisam retornar chunks na unidade de T para evitar desperdício de buffer
O motivo de usar
Uint8Arrayé alinhar com streams de bytes em nível de SONa prática, mesmo em projetos baseados em C, esse tipo de estrutura é a mais eficiente, então é natural que protocolos com informação de tipo sejam construídos sobre ela
Em versões antigas, a diferença chegava a 105x
Lembro que houve uma otimização de processamento async no Node 16, e naquela época alguns testes quebraram
Uint8Arrayexiste simUint8Arrayé simplesmente um tipo primitivo que representa um array de bytes, e a informação de tipo deve ser tratada no nível da aplicação, não do protocoloReferência: documentação de Clojure Transducers
Async iterable também não é uma solução perfeita
O overhead de Promise e de troca de stack é alto, então o desempenho é ruim ao lidar com dados em unidades pequenas
No Lit-SSR, para resolver isso, foi usada uma abordagem de incluir thunks dentro de um iterable síncrono
Só se chama e dá await no thunk quando há necessidade de trabalho async, o que melhorou o desempenho de SSR em 12 a 18 vezes
Mas, como a Streams API dificilmente adota esse tipo de estrutura contratual frágil, acho ideal uma estrutura com processamento assíncrono opcional, como
write()ewriteAsync()Compartilhei um exemplo usando generator síncrono no código do GitHub
O ponto principal é a parte
step.value.then(value => this.next(value))next(): {done, value: T} | Promise)Desde a discussão de 2013 sobre “Do not unleash Zalgo”, existe uma tendência a evitar formatos do tipo
MaybeAsync, masacho que esse medo está exagerado demais e vem impedindo projetos de API rápidos e flexíveis
Também dá para criar utilitários que puxam vários valores de uma vez, e sinto que o problema de velocidade de generator não é tão grande na prática
Lidar com Web Streams no Node.js é doloroso
Como foi projetado com foco no navegador, fica desconfortável no ambiente de servidor
Até transformações simples exigem envolver tudo em transform stream, e encadeamentos intuitivos como
.pipe()são difíceisA abordagem com async iterable é muito mais natural e combina bem com
for-await-ofA especificação de Web Streams é abstrata demais e acaba sendo pouco prática
Eu achava que isso existia apenas para compatibilidade entre cliente e servidor
O benefício real não é só desempenho, mas também a consistência entre ambientes (convergence)
Se
ReadableStreamfuncionar da mesma forma em navegador, Worker e outros runtimes,isso melhora a portabilidade do código e reduz bugs de backpressure
Padronizar a camada de streams é essencial para construir sistemas de streaming confiáveis
No passado eu criei uma abstração chamada Repeater
É um conceito que leva o construtor de Promise para async iterable, controlando eventos com push/stop
A biblioteca Repeater registra 6,5 milhões de downloads semanais, o que mostra que é estável
Recentemente tenho preferido mais streams, mas as críticas relacionadas a
tee()continuam válidasAcho que a direção certa é adotar async iterable como abstração básica
stopdo Repeater funcione tanto como função quanto como PromiseDepois de olhar o código-fonte,
pensei que, embora seja diferente do padrão tradicional, talvez tenha sido uma escolha intencional para um design ergonômico
Tenho tanta nostalgia disso que até uso “Up, Up, Down, Down, Left, Right, Left, Right, B, A” na assinatura de e-mail
Eu também já fiz um wrapper para usar AsyncIterable de forma mais concisa
É o fluent-async-iterator,
e foi útil para streaming de pequenos volumes de dados em pipelines de Lambda ou CLI
Eu esperava que, a esta altura, já existisse uma API melhor
O comportamento de backpressure de
ReadableStream.tee()é confuso porque é o oposto depipe()no Node.jsA especificação diz que “a saída mais lenta deve determinar a velocidade”, mas na implementação real até o lado rápido fica bloqueado se não for consumido
Acho melhor uma estrutura concisa baseada em push, como essa nova Stream API
Node e Web Streams usam filas infinitas para permitir chamar
res.write()de forma síncrona sem parar, masessa API força um fluxo de
yieldbaseado em generator, o que é mais seguroO problema de esgotamento do pool de conexões ao usar undici(fetch) no Node.js
acontece por causa das limitações de linguagens com garbage collection
Se você não fecha os recursos explicitamente, podem ocorrer vazamentos dependendo do momento em que o GC rodar
A abordagem de RAII (reference counting) do C++ é até mais segura
Sobre liberação de recursos, espero que o padrão
using/await usingse espalhe cada vez maisEstou aplicando aos drivers de banco uma estrutura parecida com o
usingdo C#, com suporte a dispose/disposeAsyncNúmeros de benchmark (por exemplo, 530GB/s) são difíceis de acreditar porque excedem a largura de banda de memória do M1 Pro (200GB/s)
É bem provável que sejam benchmarks feitos na base do vibe coding, sem controle adequado de qualidade da implementação