1. 導入:なぜResponse.prototype.bodyが重要なのか
Web開発において、fetch APIを使ってデータを取得する際、通常は response.json() や response.text() を使って一気にデータを取り出します。しかし、数メガバイト、あるいは数ギガバイトに及ぶ巨大なファイルを扱う場合、すべてのデータをメモリに展開するとブラウザの動作が重くなったり、最悪の場合はクラッシュしたりします。
Response.prototype.body は、データを「ストリーム(流れるデータ)」として扱うための入り口です。これを利用することで、データのダウンロード中に少しずつ処理を進めることが可能になり、メモリ効率を劇的に改善できます。
2. 基礎知識:ReadableStreamとは
Response.prototype.body が返すのは ReadableStream オブジェクトです。これは、ネットワークから届いたデータを「チャンク(断片)」ごとに読み取れるインターフェースです。
イメージとしては、蛇口から流れてくる水をバケツで少しずつ受けるようなものです。一度に全量をバケツに入れるのではなく、必要な分だけを取り出し、処理が終わったら次の分を受け取るという仕組みにより、少ないメモリで巨大なデータを扱えるようになります。
3. 実装・解決策
ストリームを読み取るには、getReader() メソッドを使用します。これにより、データを非同期に反復処理(イテレーション)できます。
基本的な手順は以下の通りです。
1. fetchでリクエストを投げる。
2. response.body からリーダーを取得する。
3. ループ内で read() を呼び出し、データが終了するまでチャンクを処理し続ける。
4. サンプルプログラム
以下は、取得したストリームからチャンクを読み取り、コンソールにバイト長を表示する実用的なコードです。
async function fetchStreamData(url) {
const response = await fetch(url);
// bodyが利用可能かチェック
if (!response.body) {
throw new Error('このブラウザはReadableStreamをサポートしていません');
}
// リーダーを取得
const reader = response.body.getReader();
let receivedLength = 0;
while (true) {
// done: データが完了したか, value: 今回読み込んだチャンク(Uint8Array)
const { done, value } = await reader.read();
if (done) break; // 読み込み完了
receivedLength += value.length;
console.log(`現在のチャンクサイズ: ${value.length} バイト`);
console.log(`合計受信済み: ${receivedLength} バイト`);
}
console.log('すべてのデータの読み込みが完了しました。');
}
// 実行例
fetchStreamData('https://example.com/large-file.zip');
5. 応用・注意点
注意点:
・一度しか読めない: ストリームは「一度読み取ると空になる」性質があります。reader.read() を使って読み取った後、再度 response.json() を呼び出そうとするとエラーになるため注意してください。
・文字化け対策: チャンクは Uint8Array(バイナリ)で届きます。テキストとして結合したい場合は、TextDecoder クラスを併用してデコードする必要があります。
現場での活用:
この技術は、プログレスバー(ダウンロード進捗)の実装に最適です。Content-Length ヘッダーから総容量を取得し、今回のチャンクサイズと合わせることで、正確なパーセンテージをリアルタイムで表示することができます。巨大なファイルを扱うモダンなフロントエンド開発では、必須のテクニックと言えるでしょう。