Streams APIとは?ReadableStream・WritableStream・TransformStreamの使い方
Streams API(Web Streams API)は、データを一度に全部読み込まず「少しずつ流しながら」処理するためのJavaScript標準APIです。ネットワーク応答・ファイル・生成AIの逐次出力のように、サイズが大きい/完了を待てないデータをメモリを抑えて扱えます。中心となるのはReadableStream(読み取り)・WritableStream(書き込み)・TransformStream(変換)の3つで、pipeThrough/pipeToでつないで処理します。なお日本語の「ストリーム処理」にはKafkaやFlinkに代表されるデータストリーム処理(リアルタイムのデータ基盤)と、本記事が扱うブラウザ/Node.jsのWeb Streams APIの2系統があり、両者は別物です(違いは後述)。
まとめ:Streams APIの要点
- 3つのストリーム:読み取り=ReadableStream、書き込み=WritableStream、変換=TransformStream。
- つなぎ方:
readable.pipeThrough(transform).pipeTo(writable)で変換と出力を連結。tee()で2系統に分岐。 - Fetch連携:
response.bodyがReadableStream。応答を待たず先頭から逐次処理できる。 - バックプレッシャー:受け手が詰まると自動で送り手を待たせる。
highWaterMarkで閾値を調整。 - 対応:主要ブラウザ・Node.js(v20以降でグローバル安定)・Deno・Cloudflare Workersで利用可。古い環境は
web-streams-polyfill。 - 使いどころ:大容量・逐次到着・変換パイプが有効。小さく一括で済むデータには不要。
ストリームとは?Streams APIが解決する課題
ストリームは「データを塊で一括処理するのではなく、届いた分から順に処理する」考え方です。従来のように全データを読み終えてから処理すると、数百MBの応答では読み込み完了までメモリを占有し、最初の1バイトを扱えるまで待たされます。ストリームなら到着したチャンク(断片)を即座に処理し、処理済みの断片は破棄できるため、メモリ使用量を一定に保てます。
Streams APIを構成する3種類のストリーム
| 種類 | 役割 | 主なメソッド |
|---|---|---|
| ReadableStream | データ源から読み取る | getReader / pipeThrough / pipeTo / tee |
| WritableStream | 宛先へ書き込む | getWriter / write / close / abort |
| TransformStream | 読みと書きを変換でつなぐ | readable / writable |
「ストリーム処理」の2つの意味(データストリーム処理とWeb Streams API)
検索で「ストリーム処理」と調べると、多くはApache Kafka・Apache Flink・Spark Streamingなどによるデータストリーム処理—イベントを到着順にリアルタイム集計し、バッチ処理と対比される分散データ基盤の話が上位に出ます。これはサーバー側のデータエンジニアリング分野の用語です。一方、本記事のWeb Streams APIはブラウザ/Node.jsのランタイムに組み込まれたJavaScript標準APIで、1プロセス内でデータを逐次処理する仕組みです。名前は似ていますが対象領域が異なるため、Kafka等の基盤を探している場合は本記事の範囲外になります。
ReadableStreamの読み取り方法
ReadableStreamは読み取り専用のデータ源です。getReader() でリーダーを取得し、read() が返す { value, done } を done が true になるまで繰り返して消費します。value にはチャンク(多くは Uint8Array)が入ります。
const reader = readable.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) break;
console.log(value.length);
}
reader.releaseLock();
for await…ofとgetReaderの使い分け
Node.jsではReadableStreamが非同期イテレータに対応し、for await (const chunk of readable) で簡潔に書けます。ブラウザは非同期イテレーションの対応が実装によって差があるため、どの環境でも確実に動かすなら getReader() + while ループが無難です。読み取りは async/await 前提のため、JavaScriptの非同期処理(async/await)の理解があるとつまずきにくくなります。
WritableStreamへの書き込み方法
WritableStreamはデータの宛先を抽象化します。getWriter() でライターを取得し、write() でチャンクを送り、最後に close() で完了、失敗時は abort() で中断します。write() が返すPromiseを待つことで、受け手の処理速度に合わせた送出(バックプレッシャー)が自然に効きます。
const writer = writable.getWriter();
await writer.write(new TextEncoder().encode("hello"));
await writer.write(new TextEncoder().encode(" world"));
await writer.close();
TransformStreamによるデータ変換
TransformStreamは読み取り側(readable)と書き込み側(writable)をひと組で持ち、書き込まれたチャンクを変換して読み取り側へ流すストリームです。ReadableStream.pipeThrough() に渡して変換段として差し込みます。TextDecoderStream や CompressionStream はTransformStreamの形をした組み込み実装です。
自作TransformStreamの実装例
コンストラクタに transform(chunk, controller) を渡すと、チャンクごとの変換処理を定義できます。次は文字列を大文字化する変換段です。
const upper = new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk.toUpperCase());
}
});
Firefox 102以降でTransformStreamがクロスブラウザ対応となり、現在は主要ブラウザとNode.jsで共通して使えます。
pipeThrough・pipeTo・teeによるパイプ処理
ストリームは手続き的に読み書きするより、パイプでつないで宣言的に連結する方が読みやすくなります。pipeThrough(transform) で変換段を挟み、pipeTo(writable) で最終的な宛先へ流します。パイプはバックプレッシャーとエラー伝播を自動で扱うため、途中で失敗すると連結全体が適切に中断されます。
await response.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(upper)
.pipeTo(writable);
同じデータを2系統に分けたいときは tee() を使い、1本のReadableStreamを2本に複製します(例:一方をキャッシュ保存、もう一方を画面表示)。
Fetch APIとの連携(レスポンスの逐次処理)
Fetch APIの応答オブジェクトの response.body はReadableStreamです。await response.text() のように全body受信を待つ代わりに、到着した断片から順に処理できます。生成AIのトークン逐次出力や大きなJSON・ログの逐次描画で効果があります。
const res = await fetch("https://example.com/large.json");
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
while (true) {
const { value, done } = await reader.read();
if (done) break;
render(value);
}
リクエスト側のbodyにReadableStreamを渡すアップロードのストリーミングも可能ですが、fetch に duplex: "half" の指定が必要で、HTTP/2などサーバー側の対応も前提になります。全body受信を待つ従来の response.json() と違い、応答の先頭から段階的にUIへ反映できるのが逐次処理の利点です。
バックプレッシャーの制御
バックプレッシャーは、受け手の処理が追いつかないとき送り手を自動的に待たせる仕組みです。これがないと未処理データがメモリに溜まり続けます。Streams APIではキュー戦略(queuing strategy)の highWaterMark が閾値で、キューがこれを超えると送り手側の desiredSize が0以下になり、書き込みPromiseの解決が遅れて自然に流量が絞られます。
const readable = new ReadableStream(source, new CountQueuingStrategy({ highWaterMark: 3 }));
// controller.desiredSize が 0 以下なら enqueue を控える
バックプレッシャーで気をつける点
pipeThrough/pipeTo を使えばバックプレッシャーは自動で伝播するため、通常は自前の制御は不要です。手動で read()/write() を回す場合は、write() のPromiseを必ず await して受け手の完了を待つこと(待たずに write() を連打するとバックプレッシャーが効かずメモリが膨張します)。
Compression Streams APIによる圧縮・展開
Compression Streams APIは、外部ライブラリなしでブラウザ標準の圧縮・展開を行うAPIです。CompressionStream/DecompressionStream はTransformStreamと同じ形をしており、pipeThrough にそのまま差し込めます。対応形式は gzip・deflate・deflate-raw で、圧縮ライブラリをバンドルしない分アプリの配信サイズを削減できます(Brotli・zstd等の追加形式は実装差があるため最新は公式で確認してください)。
const compressed = new Response("...").body
.pipeThrough(new CompressionStream("gzip"));
圧縮ロジックを自前で持たずにアップロード前圧縮やキャッシュ縮小ができる点が実務上の利点で、現在は全モダンブラウザとNode.js・Denoで利用できます。
ブラウザ・Node.jsの対応状況とpolyfill
ReadableStream・WritableStream・TransformStreamはいずれも主要ブラウザで利用可能です。Node.jsでは node:stream/web から利用でき、v20以降はグローバルの安定APIとして ReadableStream 等がそのまま使えます(Deno・Cloudflare Workersも対応)。個別メソッド(ReadableStream.from() やバイトストリーム=BYOBリーダーなど)は対応時期に差があるため、使用前にMDNの互換性表を確認してください。
web-streams-polyfillが必要な場面
古いブラウザや一部の実行環境でStreams APIが未対応、または新しいメソッドが欠ける場合は web-streams-polyfill で補えます。逆に、対応環境しか想定しないなら追加は不要です。小さく一括取得すれば済むデータにStreams APIを持ち込むのは過剰設計で、コードが複雑になるだけです。大容量・逐次到着・変換パイプが必要な場面に絞って採用するのが実務的な判断基準になります。
よくある質問
ストリーム処理とストリーミングの違いは何ですか?
文脈で指すものが異なります。データ基盤の文脈での「ストリーム処理」はKafka等でイベントを到着順に処理する方式(バッチ処理の対義)を指し、Web開発での「ストリーミング」は動画・音声・API応答を逐次受信・再生することを指します。本記事のWeb Streams APIは後者を含む、ブラウザ/Node.jsで逐次データを扱う標準APIです。
ReadableStreamはfor await…ofで読み取れますか?
Node.jsでは対応しており for await (const chunk of readable) で読めます。ブラウザは非同期イテレーションの対応に差があるため、環境を問わず動かすなら getReader() + while ループが確実です。
Fetch APIのレスポンスをストリームで受け取るには?
response.body がReadableStreamなので、response.body.getReader() で取得するか、pipeThrough(new TextDecoderStream()) でテキスト化して逐次読み取ります。response.text() と違い全body受信を待たずに処理を始められます。
Streams APIはNode.jsで使えますか?
使えます。v20以降は ReadableStream・WritableStream・TransformStream がグローバルの安定APIとして利用でき、明示的に読み込む場合は node:stream/web からインポートします。従来のNodeストリーム(streamモジュール)とは相互変換のユーティリティも用意されています。
web-streams-polyfillはいつ必要ですか?
Streams API未対応の古い環境や、ReadableStream.from() のような新しいメソッドが欠ける環境を対象に含める場合に導入します。対応済みの環境だけを想定するなら不要です。