4 pontos por GN⁺ 2026-02-28 | 1 comentários | Compartilhar no WhatsApp
  • 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

 
GN⁺ 2026-02-28
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 que next() possa retornar tanto resultados síncronos quanto assíncronos
    Com 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

    • Você diz que a sua abordagem é melhor, mas na verdade acho que a deles é superior por ser uma forma primitiva mais fundamental
      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 conceito de stream proposto é interessante, mas o design deles parte da compatibilidade com AsyncIterator
      O motivo de usar Uint8Array é alinhar com streams de bytes em nível de SO
      Na 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
    • Medi em microbenchmark a diferença de velocidade entre chamadas de função síncronas e async no Node 24, e ficou algo em torno de 90x mais lento
      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
    • O tipo Uint8Array existe sim
      Uint8Array é 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 protocolo
    • Essa estrutura é parecida com o conceito de transducer do Clojure
      Referê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() e writeAsync()

    • O meu stream iterator pode resolver o problema que você mencionou
      Compartilhei um exemplo usando generator síncrono no código do GitHub
      O ponto principal é a parte step.value.then(value => this.next(value))
    • Gostei da proposta do conartist6 (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, mas
      acho 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íceis
    A abordagem com async iterable é muito mais natural e combina bem com for-await-of
    A especificação de Web Streams é abstrata demais e acaba sendo pouco prática

    • Surpreende saber que há quem realmente use Web Streams no Node
      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 ReadableStream funcionar 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

    • Sim, o grande valor não é apenas desempenho, mas sim a padronização
  • 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álidas
    Acho que a direção certa é adotar async iterable como abstração básica

    • Achei interessante que o stop do Repeater funcione tanto como função quanto como Promise
      Depois 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
    • Não tem a ver com o tema, mas fiquei muito feliz de ver o exemplo do código Konami
      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 de pipe() no Node.js
    A 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, mas
    essa API força um fluxo de yield baseado em generator, o que é mais seguro

  • O 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 using se espalhe cada vez mais
    Estou aplicando aos drivers de banco uma estrutura parecida com o using do C#, com suporte a dispose/disposeAsync

  • Nú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