Web開発者のためのStreams解説
fetch()を呼び出してレスポンスを待つとき、ブラウザはすでにそのデータを断片的に受信しています。Web Streams APIは、レスポンス全体が到着してから触れるのを待つのではなく、到着した断片にJavaScriptコードからアクセスできるようにします。
この転換 — 「すべてを待つ」から「到着したものから処理する」へ — がストリームの本質です。
重要なポイント
- Web Streams APIを使うと、レスポンス全体をメモリにバッファリングするのではなく、データが到着するたびに段階的に処理できます。
ReadableStream、WritableStream、TransformStreamが3つのコアプリミティブで、データパイプラインを構築するための組み合わせ可能なビルディングブロックです。fetch()からのresponse.bodyが最も一般的なエントリーポイントで、チャンクごとに読み取れるReadableStreamです。pipeThrough()とpipeTo()を使って変換と出力を連鎖させることができ、バックプレッシャー処理が自動的に組み込まれています。
すべてを一度に読み込むことが問題である理由
従来のデータ取得アプローチは次のようなものです:
const response = await fetch('/large-dataset.json')
const data = await response.json()
// Nothing happens until all bytes are downloaded and parsed
小さなペイロードであれば問題ありません。しかし50MBのJSONファイルや長時間実行されるAPIレスポンスの場合、1つのレコードも処理する前に全体をメモリに保持することになります。制約のあるデバイスや低速な接続では、これはUIの遅延、高いメモリ圧迫、そしてユーザーのフラストレーションを意味します。
ストリームを使えば、最初のチャンクが到着した瞬間からデータの処理を開始できます。
Web Streams APIの3つのコアプリミティブ
Web Streams APIは3つのクラスを中心に構築されています:
ReadableStream— データを読み取るソースWritableStream— データを書き込む宛先TransformStream— 中間に位置し、一方から読み取って変換されたデータをもう一方に書き込む
データはこれらのストリームを通じてチャンクとして移動します — 一度に1つずつ処理される小さな断片です。チャンクは、ストリームに応じて、バイトのUint8Array、文字列、または任意のJavaScript値になります。
Fetchストリーミング: レスポンスの段階的読み取り
ほとんどのfetch()レスポンスは、response.bodyを介してボディをReadableStreamとして公開します。これはフロントエンド開発者にとって、JavaScriptストリームへの最も一般的なエントリーポイントです。
async function processLargeResponse(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
console.log(decoder.decode(value, { stream: true }))
}
} finally {
reader.releaseLock()
}
}
reader.read()は{ value, done }で解決されるpromiseを返します。doneがtrueのとき、ストリームは終了しています。このパターンにより、全体をバッファリングすることなく、数メガバイトのレスポンスをチャンクごとに処理できます。
リクエストボディのストリーミングに関する注意:
ReadableStreamをfetch()のリクエストボディとして渡すことは可能ですが、ブラウザサポートにばらつきがあります。レスポンスのストリーミングは、今日使える十分にサポートされた実用的なパターンです。
Discover how at OpenReplay.com.
pipeThrough()とpipeTo()によるデータパイプラインの構築
ストリームが真に強力になるのは合成です。ReadableStreamを1つ以上のTransformStreamインスタンスを通して連鎖させ、結果をWritableStreamにパイプできます。
fetch('./data.txt').then((response) =>
response.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk.toUpperCase())
}
}))
.pipeTo(new WritableStream({
write(chunk) {
document.body.textContent += chunk
}
}))
)
このパイプラインは、バイトをテキストにデコードし、各チャンクを大文字に変換してから、DOMに書き込みます — すべて段階的に、完全なレスポンスを待つことなく実行されます。
pipeThrough()はReadableStreamをTransformStreamに接続し、新しいReadableStreamを返します。pipeTo()はReadableStreamをWritableStreamに接続し、ストリームが完了したときに解決されるpromiseを返します。
バックプレッシャー: ストリームが過負荷を回避する方法
コンシューマーがプロデューサーがデータを生成するよりも遅くデータを処理する場合、ストリームはバックプレッシャーを適用します — パイプチェーンを通じて伝播する信号で、ソースに速度を落とすよう指示します。これはpipeTo()とpipeThrough()を使用すると自動的に発生します。これが、ループ内で手動でチャンクを読み取るよりもパイピングを優先すべき主な理由の1つです。
知っておくべき組み込みストリーム
ブラウザにはいくつかの既製のストリームユーティリティが搭載されています:
TextDecoderStream/TextEncoderStream— バイトと文字列の間で変換CompressionStream/DecompressionStream— データをその場でgzipまたはdeflateBlob.stream()— 任意のBlobまたはFileをReadableStreamとして読み取る
最新のNode.jsもWeb Streams APIをサポートしているため、ブラウザ用に構築したパイプラインはサーバーサイド環境にもきれいに移植できます。
まとめ
Web Streams APIは、フロントエンド開発者に、時間をかけて到着するデータを処理するための、組み合わせ可能でメモリ効率の良い方法を提供します。ReadableStreamとTransformStreamは最もよく使うプリミティブです — 特に段階的なレスポンス処理のためにfetch()と組み合わせる場合に。response.bodyから始め、データを変換する必要があるときはpipeThrough()を使い、フロー制御はバックプレッシャーに任せましょう。
よくある質問
はい。ReadableStream、WritableStream、TransformStream、およびパイピングメソッドは、Chrome、Firefox、Safari、Edgeを含むすべての最新ブラウザでサポートされています。response.bodyを介したfetchレスポンスボディのストリーミングも広くサポートされています。fetchでのリクエストボディのストリーミングはサポートがより限定的なので、その機能に依存する前に互換性テーブルを確認してください。
パイプチェーン内のいずれかの段階でエラーがスローされると、エラーはパイプライン全体に伝播します。読み取り可能側はエラー状態になり、書き込み可能側は中止されます。これは、signalを含むoptionsオブジェクトを渡すか、pipeToから返されるpromiseをキャッチすることで処理できます。手動読み取りループの場合は、readの呼び出しをtry-catchブロックでラップしてください。
Node.jsは当初、Readable、Writable、Transformクラスを持つ独自のストリームAPIを提供していました。Web Streams APIはブラウザ向けに設計された別の標準です。最新バージョンのNode.jsは両方をサポートしています。Web Streams APIはpromiseを使ったプルベースのモデルを使用し、従来のNodeストリームはイベントベースのプッシュモデルを使用します。Web Streams APIに対して書かれたコードは、ブラウザとサーバー環境の両方で移植可能です。
レスポンスが小さい場合、例えば数百キロバイト以下であれば、response.jsonやresponse.textでバッファリングする方がシンプルで完全に効率的です。ストリームは、大きなペイロード、リアルタイムデータ、または完全なレスポンスが到着する前に部分的な結果を表示したい状況で価値を発揮します。コンパクトなJSONを返す単純なAPI呼び出しの場合、従来のアプローチで問題ありません。
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.