JavaScript GeneratorとAsyncIteratorを用いたストリームデータ処理

JavaScript Frontend Backend Performance
結論

非同期イテレーションには async function*for await...of を使い、finally で解放します。

// MDN公式仕様:AsyncGenerator によるストリームChunk取得とリソース解放
async function* fetchStreamChunks(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      yield decoder.decode(value, { stream: true });
    }
  } finally {
    // ⭕ ループ途中 break やエラー発生時にも必ずクローズ処理を実行
    reader.releaseLock();
  }
}

MDN公式仕様:AsyncIterator プロトコルの評価比較

評価軸配列一括読み込み (await fetch().json())AsyncIterator ストリーム処理
メモリ消費量❌ データサイズに比例して一気に肥大化 (OOMリスク)◯ 1Chunk分(数KB)のみで極小一定
画面初回表示(TTFB)❌ 全データ転送完了まで待機◯ 最初のChunk到着時点で即表示開始
反復構文for (const item of array)for await (const chunk of generator)

実際に起こる事故:break や例外時におけるリソースリーク

MDN公式ドキュメント(AsyncGenerator Protocol)で規定されている通り、for await...of ループ処理の途中で break や例外スロー(throw)が発生した場合、JSエンジンはイテレータの .return() メソッドを呼び出してループを中断します。

この時、async function* 内で try...finally を記述していないと、WebソケットやDBカーソル、Fetchの getReader() ロックが解放されずに残り続け、サーバーがメモリリーク(OOM)を起こす障害が発生します。


実装手順

  1. async function* 構文で非同期ジェネレータ関数を宣言する
  2. 内部のリソース生成・読み込み処理を try { ... } finally { ... } ブロックで囲む
  3. yield キーワードで1データ単位(Chunk)を呼び出し元にストリーミング返却する
  4. 呼び出し側は for await (const chunk of fetchStreamChunks(url)) で安全にループ処理する