4 ポイント 投稿者 GN⁺ 2026-02-28 | 1件のコメント | WhatsAppで共有
  • Web Streams 標準は、ブラウザとサーバー間で一貫したデータストリーミングを実現するために設計されたが、現状では複雑さと性能上の限界により開発者体験が損なわれている
  • 既存 API は、ロック(lock)管理、BYOB、バックプレッシャー(backpressure) などの設計上の制約により、使い勝手と実装の両面で不要な負担を生んでいる
  • Cloudflare は、非同期反復(async iteration) ベースの新しいストリームモデルを提案しており、この方式は2倍から最大120倍高速な性能を示している
  • 新 API は、シンプルな async iterable 構造明示的なバックプレッシャーポリシー同期/非同期の並行サポートにより、効率性と一貫性を高める
  • このアプローチは、Node.js、Deno、Bun、ブラウザなどすべてのランタイムで統一的なストリーミングモデルを可能にし、今後の標準化議論の出発点となり得る

Web Streams の構造的限界

  • WHATWG Streams 標準は 2014〜2016 年に開発され、ブラウザ中心に設計されたため、当時はasync iteration が存在せず、別個の reader/writer モデルが導入された
    • その結果、ロック管理、複雑な読み取りループ、BYOB バッファ処理などの不要な手続きが生まれた
  • ロック(locking)モデルはストリームを排他的に占有して並列消費を妨げ、releaseLock() の呼び出し忘れでストリームが永続的にロックされる問題が起こる
  • BYOB(Bring Your Own Buffer) 機能はメモリ再利用を目的としていたが、複雑なバッファ分離・転送モデルのため実際の活用度は低く、実装難易度も高い
  • バックプレッシャー(backpressure) は理論上サポートされているが、desiredSize の値が負でも enqueue() が成功するなど、実際には制御できない構造になっている
  • read() 呼び出しごとに Promise の生成が強制されるため、高頻度ストリーミングでは性能低下と GC 負荷を引き起こす

実務で明らかになった問題

  • fetch() のレスポンス本文を消費しないと コネクションプールの枯渇が発生し、tee() 使用時には 無制限のメモリバッファリングが起こる
  • TransformStream読み取り準備の有無に関係なく即時処理されるため、遅いコンシューマ環境ではバッファの急増を招く
  • サーバーサイドレンダリング(SSR)では、数千個の小さなチャンク処理による GC スラッシングが発生し、性能が急激に低下する
  • 各ランタイム(Node.js、Deno、Bun、Workers)はこれを緩和するために非標準の最適化パスを導入したが、その結果互換性と一貫性の低下が生じた
  • Web Platform Tests は 70 個以上の複雑なテストファイルを要求しており、これは過剰な内部状態管理と直感的でない挙動の結果である

新しいストリーム API の設計原則

  • ストリームはシンプルな async iterable として定義され、for await...of で直接消費できる
  • Pull-through 変換を採用し、コンシューマがデータを要求したときだけ処理を実行する
  • 明示的なバックプレッシャーポリシー(strict, block, drop-oldest, drop-newest) を提供し、メモリ暴走を防ぐ
  • バッチチャンク(Uint8Array[]) 単位でデータを渡し、Promise 生成コストを減らす
  • バイト単位専用処理に簡素化し、BYOB や複雑なコントローラー概念を取り除く
  • 同期(synchronous)パスのサポートにより、CPU 中心の処理で Promise オーバーヘッドを除去する

新 API の例と特徴

  • Stream.push() で簡単に writer/readable ペアを生成でき、Stream.text() で全文テキストを収集できる
  • Stream.pull()遅延(lazy)パイプラインを構成し、消費時点でのみ実行される
  • Stream.share()Stream.broadcast()明示的なマルチコンシューマ管理をサポートする
  • Sync/Async 並行 API(Stream.pullSync(), Stream.textSync()) により、I/O のない演算で性能を最大化する
  • Web Streams との相互運用のため、シンプルなアダプター関数で変換できる

性能比較と展望

  • Node.js 基準のベンチマークでは最大 80〜90 倍、ブラウザでは最大 100 倍以上高速な処理速度が確認された
    • 例: 3 段の変換チェーンで 275GB/s vs 3GB/s
  • 性能向上は、非同期オーバーヘッドの除去、バッチ処理、pull ベース設計に起因する
  • この実装は純粋な TypeScript/JavaScript で書かれており、ネイティブ実装ではさらなる向上の可能性がある
  • Cloudflare はこのアプローチを標準化議論の出発点として提示し、開発者コミュニティからのフィードバックを求めている

結論

  • Web Streams は当時の制約下では合理的だったが、現代の JavaScript の言語機能や開発パターンに合っていない
  • 新しい async iterable ベースのモデルは、シンプルさ、性能、明示的制御をすべて満たし、ランタイム間で一貫したストリーミングエコシステムを構築できる可能性を示している
  • Cloudflare は GitHub の jasnell/new-streams参照実装、ドキュメント、サンプルコードを公開している
  • 目標は新たな標準を策定することではなく、**「より良いストリーム API」**を議論するための実質的な出発点を用意することにある

1件のコメント

 
GN⁺ 2026-02-28
Hacker Newsのコメント
  • この記事で提案されているAPIよりも優れたStreamインターフェースを自分で設計した
    既存の提案は async iterator of UInt8Array という形だが、私は next() が同期・非同期の結果をどちらも返せる構造を提案している
    こうすることで
    既存の構造より単一のイテレータでシンプルに反復できる
    同期入力に同期変換を適用すれば、処理全体を同期で完結でき、コードの重複を減らせる
    不要なPromise生成が減って性能向上が見込める
    並行性制御が可能になり、async iteratorの限界を克服できる

    • あなたの提案の方が良いという話だが、実際には相手の方式の方が、より基本的なプリミティブとして優れていると思う
      あなたの方式から彼らの構造を簡単に作ることはできないが、逆は可能だ
      I/O中心のイテレータは、バッファの無駄を防ぐためにT単位のチャンクを返す必要がある
    • 提案されたストリームの概念は興味深いが、彼らの設計はAsyncIterator互換性を前提にしている
      Uint8Array を使う理由は、OSレベルのバイトストリームに合わせるためだ
      実際、Cベースのプロジェクトでもこうした構造が最も効率的なので、型情報を持つプロトコルはその上に積み上げる形が自然だ
    • Node 24で同期関数呼び出しとasync関数呼び出しの速度差をマイクロベンチマークで測定したところ、約90倍遅かった
      以前のバージョンでは105倍まで差があった
      async処理の最適化がNode 16で入って、そのとき一部のテストが壊れた記憶がある
    • Uint8Array という型は存在しない
      Uint8Array は単にバイト配列を表すプリミティブ型であり、型情報はプロトコルではなくアプリケーションレベルで扱うべきだ
    • この構造はClojureのtransducerの概念に似ている
      参考: Clojure Transducersドキュメント
  • Async iterableも完璧な解決策ではない
    Promiseとスタック切り替えのオーバーヘッドが大きく、小さな単位のデータを扱うときは性能が悪い
    Lit-SSRではこれを解決するために、同期iterableの中にthunkを含める方式を使っていた
    async処理が必要なときだけthunkを呼び出してawaitすることで、SSR性能を12〜18倍改善した
    ただしStreams APIではこのような壊れやすい契約構造を採用しにくいため、write()writeAsync() のように選択的な非同期処理が可能な構造が理想的だと思う

    • あなたが言う問題は、私のstream iteratorで解決できる
      同期generatorを活用した例をGitHubコードで共有する
      核心は step.value.then(value => this.next(value)) の部分だ
    • conartist6の提案(next(): {done, value: T} | Promise)が気に入った
      2013年の「Do not unleash Zalgo」論争以降、MaybeAsync 形式を避ける傾向があったが
      この恐れはあまりに誇張されていて、高速で柔軟なAPI設計を妨げていると思う
      複数の値を一度に取り込むユーティリティも作れるし、generatorの速度問題も実際にはそれほど大きくないと感じる
  • Node.jsでWeb Streamsを扱うのはつらい
    ブラウザ中心に設計されているため、サーバー環境では使いづらい
    単純な変換でもtransform streamで包む必要があり、.pipe() のような直感的なチェイニングが難しい
    Async iterableアプローチの方がはるかに自然で、for-await-of とも相性が良い
    Web Streams仕様はあまりに抽象化偏重で、実用性に欠ける

    • NodeでWeb Streamsを実際に使っている人がいるのは驚きだ
      私はそれが単なるクライアント–サーバー間互換性用だと思っていた
  • 本当の利点は性能だけでなく、**環境間の一貫性(convergence)**にある
    ReadableStreamがブラウザ、Worker、その他のランタイムで同じように動作すれば
    コード移植性が高まり、backpressureバグの減少にもつながる
    ストリーム層の標準化は、信頼できるストリーミングシステムを構築するうえでの核心だ

    • その通り、単なる性能ではなく標準化の価値が大きい
  • 以前、Repeaterという抽象化を作ったことがある
    Promiseコンストラクタをasync iterableに移したような概念で、イベントをpush/stopで制御する
    Repeaterライブラリは週650万ダウンロードを記録するほど安定している
    最近はstreamsの方を好むが、tee() に関する批判は今でも有効だ
    async iterableを基本抽象化にする方向が正しいと思う

    • Repeaterの stop が関数でありPromiseでもあるのが興味深かった
      ソースコードを見てから
      伝統的なパターンとは違うが、人間工学的な設計のための意図的な選択かもしれないと思った
    • 話題とは外れるが、コナミコマンドの例がとても懐かしかった
      メール署名にも「Up, Up, Down, Down, Left, Right, Left, Right, B, A」と書くほど思い入れがある
  • 私もAsyncIterableをより簡潔に使うためのラッパーを作ったことがある
    fluent-async-iteratorで、
    LambdaやCLIパイプラインでの小規模データストリーミングに役立った
    今ごろはもっと良いAPIが出ていてほしかった

  • ReadableStream.tee()backpressureの挙動はNode.jsの pipe() と逆なので混乱する
    仕様書には「最も遅い出力が速度を決めるべき」とあるのに、実際の実装では速い側が消費されなくても詰まってしまう
    新しいStream APIのようなpushベースの簡潔な構造の方が良いと思う
    NodeとWeb Streamsは無限キューを置くことで、同期的に res.write() を乱発できるようにしているが、
    このAPIはgeneratorベースのyieldフローを強制するので、より安全だ

  • Node.jsでundici(fetch) を使う際にコネクションプール枯渇の問題が起きるのは
    ガベージコレクション言語の限界のためだ
    明示的にリソースを閉じないと、GCのタイミング次第でリークが発生する
    C++のRAII(reference counting) アプローチの方がむしろ安全だ

  • リソース解放に関しては、using/await using パターンがもっと広まってほしい
    C#の using のようにdispose/disposeAsyncをサポートする構造をDBドライバに適用しているところだ

  • ベンチマークの数値(例: 530GB/s)はM1 Proのメモリ帯域幅(200GB/s)を超えているので、信頼しがたい
    実装品質の管理が不十分な
    vibe-codedベンチマーク
    である可能性が高い