Fetch を使ってブラウザにデータをストリーミングする方法
ほとんどの Fetch API チュートリアルでは同じパターンが紹介されています。fetch() を呼び出し、レスポンスを待機し、.json() または .text() を呼び出して完了です。これは小さなペイロードには問題なく機能します。しかし、サーバーがデータを段階的に生成している場合—AI レスポンス、ライブログ、大規模なデータセットなどを考えてみてください—1バイトにも触れる前にレスポンス全体を待つのは本当に問題です。
良いニュース:Fetch API はすでにブラウザでの増分データストリーミングをサポートしています。その使い方を説明します。
重要なポイント
- Fetch API の
response.bodyはReadableStreamを公開しており、完全なペイロードを待つのではなく、データが到着するたびにチャンクごとに処理できます。 - ストリーミングレスポンスを読み取る際は、最も広範なブラウザ互換性のために
response.body.getReader()とTextDecoderを使用してください。 - ネットワークチャンクはメッセージ境界を尊重しません—改行区切り JSON のような構造化フォーマットを解析する際は、不完全な行をバッファリングして自分で分割する必要があります。
- ユーザーが離脱したときにリクエストをクリーンにキャンセルできるよう、長時間実行されるストリームには常に
AbortControllerを組み合わせてください。
Fetch API によるストリーミングレスポンスが重要な理由
response.json() または response.text() を呼び出すと、ブラウザは Promise を解決する前にレスポンスボディ全体を受信する必要があります。50MB のログファイルや低速な AI 補完エンドポイントの場合、これはアプリケーションが最後のバイトが到着するまでレスポンスの処理やレンダリングができないことを意味します。
ストリーミングを使用すると、データが到着するたびに処理できます—残りのデータがまだ転送中でも、最初のチャンクをユーザーに表示できます。これは体感パフォーマンスの意味のある改善です。
ReadableStream Fetch API の仕組み
すべての fetch() レスポンスは response.body に ReadableStream を公開します。完全なペイロードを待つ代わりに、リーダーをアタッチし、ネットワークから送られてくるチャンクをプルします。
最も広く互換性のあるアプローチは response.body.getReader() です:
const response = await fetch('/api/stream')
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`)
}
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { value, done } = await reader.read()
if (done) break
console.log(decoder.decode(value, { stream: true }))
}
各 value は生のバイトの Uint8Array です。TextDecoder はこれらのバイトを文字列に変換します。{ stream: true } を渡すことで、チャンク境界をまたいで分割される可能性のあるマルチバイト文字をデコーダーが正しく処理できます。
非同期イテレーションに関する注意:
for await (const chunk of response.body)という構文を見たことがあるかもしれません。この構文はよりクリーンですが、バージョン 18.x 時点では Safari でサポートされていないため、上記のgetReader()ループが本番環境ではより安全な選択です。現在のブラウザサポートは https://caniuse.com/wf-async-iterable-streams で確認できます。
TextDecoderStream によるテキストストリームのデコード
パイプラインスタイルのアプローチを好む場合、TextDecoderStream がデコードを自動的に処理します:
const response = await fetch('/api/stream')
const reader = response.body
.pipeThrough(new TextDecoderStream())
.getReader()
while (true) {
const { value, done } = await reader.read()
if (done) break
console.log(value) // すでに文字列
}
これは複数の変換ステップをチェーンする際により明確です。
Discover how at OpenReplay.com.
Fetch によるブラウザストリーミングの実践的な考慮事項
チャンク境界は任意です。 ネットワークチャンクは行やメッセージと整列しません。改行区切り JSON や SSE イベントを解析する場合、不完全な行をバッファリングし、自分で \n で分割する必要があります。
ストリームは一度しか消費できません。 getReader() でリーダーをアタッチすると、ストリームはそのリーダーにロックされ、データが読み取られるとボディは disturbed 状態になり、再度消費できなくなります。ボディを2か所で必要とする場合は、読み取る前に response.clone() を呼び出してください:
const response = await fetch('/api/data')
const clone = response.clone()
// オリジナルをストリームとして読み取る
const reader = response.body.getReader()
// クローンを別の場所で通常通り使用する
const text = await clone.text()
AbortController でストリームをキャンセルする。 長時間実行されるストリームはキャンセル可能であるべきです—特にユーザーが離脱する場合:
const controller = new AbortController()
const response = await fetch('/api/stream', {
signal: controller.signal
})
// 必要に応じてキャンセル
controller.abort()
これにより、誰も読んでいないデータをブラウザが受信し続けることを防ぎます。
まとめ
Fetch によるブラウザストリーミングは、現在十分にサポートされており実用的です。コアパターンは簡単です:response.body からリーダーを取得し、reader.read() でループし、TextDecoder でバイトをデコードし、独自のバッファでチャンク境界を処理します。クリーンアップのために AbortController を追加し、複数の場所でデータが必要な場合はレスポンスボディが一度しか消費できないことに注意してください。これで、ブラウザでレスポンシブで増分的なデータ体験を構築するために必要なすべてが揃いました。
よくある質問
Fetch ストリーミングは、POST、PUT、PATCH を含む、レスポンスボディを返すすべての HTTP メソッドで機能します。response.body の ReadableStream は、使用されるメソッドに関係なく同じように動作します。増分読み取りが意味を持つためには、サーバーがチャンクまたはストリーミングレスポンスを送信する必要があるだけです。
文字列バッファを維持する必要があります。デコードされた各チャンクをバッファに追加し、改行文字で分割します。完全な行をすべて JSON として処理し、末尾の不完全なセグメントを次のチャンクのためにバッファに保持します。これは、ネットワークチャンクが JSON オブジェクトを2つの読み取りに分割する可能性があるという事実を考慮しています。
はい。チャンクから text/event-stream フォーマットを手動で解析することで、Fetch ストリーミング経由で SSE エンドポイントを消費できます。これにより、GET リクエストのみをサポートし、ヘッダーのカスタマイズが制限されている EventSource API と比較して、ヘッダー、認証、リクエストメソッドをより細かく制御できます。
接続が切断されるか、ストリームがエラーになった場合、reader.read() によって返される Promise は reject されます。アプリケーションが適切に失敗を処理し、ユーザーに通知したり、必要に応じてリクエストを再試行できるように、読み取りループを try-catch ブロックでラップしてください。
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.