XMLHttpRequestとFetch APIの機能差および移行における制約の整理
Web標準における非同期通信の主流はXMLHttpRequest (以下、XHR) からFetch APIへと移行しましたが、ユースケースによっては現在でもXHRを選択せざるを得ない領域が存在します。本稿では主要な3つの機能差 (キャンセル、ダウンロード進捗、アップロード進捗) について、最新の仕様および実用上の制約を踏まえて比較と解説します。
各機能の詳細と実装比較
リクエストのキャンセル
Fetch APIの登場当初はリクエストの中断手段がありませんでしたが、現在はAbortControllerの導入により、XHRのxhr.abort()と同等以上の柔軟な制御が可能です。
XHRによる実装
const xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/data');
xhr.send();
// 任意のタイミングで中断
xhr.abort();Fetch APIによる実装
const controller = new AbortController();
try {
const response = await fetch('/path/to/data', { signal: controller.signal });
const data = await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.log('リクエストがキャンセルされました。');
}
}
// 任意のタイミングで中断
controller.abort();- 現状:
AbortControllerは主要ブラウザおよびNode.jsなどのサーバーサイド環境にも広く普及しており、完全にFetch APIへ移行可能です。
ダウンロード進捗の取得
レスポンスの受信進捗を表示する機能です。XHRの手軽さに比べ、Fetch APIはストリームを扱うため記述量が増えますが、非同期反復 (Async Iterable) のサポートにより実用的なコードで実装可能です。
XHRによる実装
const xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/large-file.json');
xhr.onprogress = (event) => {
if (event.lengthComputable) {
const percent = (event.loaded / event.total) * 100;
console.log(`ダウンロード進捗: ${percent}%`);
}
};
xhr.send();Fetch APIによる実装
const response = await fetch('/path/to/large-file.json');
const total = parseInt(response.headers.get('content-length') || '0', 10);
let loaded = 0;
const progressStream = new ReadableStream({
async start(controller) {
// response.body の非同期反復(for await...of)を利用してスマートに記述可能
for await (const chunk of response.body) {
loaded += chunk.byteLength;
const percent = total ? ((loaded / total) * 100).toFixed(1) : 0;
console.log(`ダウンロード進捗: ${percent}%`);
controller.enqueue(chunk);
}
controller.close();
}
});
const data = await new Response(progressStream).json();- 現状: 記述の簡潔さではXHRに劣りますが、Fetch APIの
ReadableStreamを用いることでサードパーティ製ライブラリを頼らずに完全な代替が可能です。
アップロード進捗の取得
大容量ファイルの送信時などに進捗バーを表示する機能です。Chromium系ブラウザを中心にFetch Upload Streaming (ボディにストリームを渡す仕様) が追加されましたが、実務においては現在でもXHRから状況が変わっていない (XHRを使わざるを得ない) 最大の要因がここにあります。
XHRによる実装 (FormDataによる複数ファイル送信)
XHRではFormDataを使って複数ファイルやテキストパラメータを同時に送信しても、それらを含めたリクエスト全体の送信物理量をたった1つのイベントで正確に自動計測してくれます。
const formData = new FormData();
formData.append('userId', '12345');
formData.append('files', fileA);
formData.append('files', fileB);
const xhr = new XMLHttpRequest();
xhr.open('POST', '/upload');
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
const percent = (event.loaded / event.total) * 100;
console.log(`全体のアップロード進捗: ${percent.toFixed(1)}%`);
}
};
xhr.send(formData);Fetch APIによる実装 (Fetch Upload Streaming)
Fetch APIで進捗を取得するにはbodyにReadableStreamを直接渡し、JavaScript側でチャンクの消費量をカウントする必要があります。
const total = file.size;
let uploaded = 0;
const uploadStream = new ReadableStream({
async start(controller) {
// file.stream() から for await...of でチャンクを取り出して監視
for await (const chunk of file.stream()) {
uploaded += chunk.byteLength;
console.log(`アップロード進捗: ${((uploaded / total) * 100).toFixed(1)}%`);
controller.enqueue(chunk);
}
controller.close();
}
});
await fetch('/upload', { method: 'POST', body: uploadStream, duplex: 'half' });Fetch APIによるアップロード進捗取得が「実用解」にならない4つの重大な制約
上記の通りFetch APIでも実装自体は可能となりましたが、プロダクション環境においてXHRを置き換えるのは極めて困難です。その理由は以下の通りです。
1. FormData (複数ファイルやパラメータの同時送信) が一切使えない
Fetchで進捗を取るためには body に ReadableStream を直接指定しなければなりません。これは単一のバイナリを流すことを意味するため、ブラウザが境界線を自動生成してくれる FormData オブジェクトを組み合わせることが不可能です。
もし複数のファイルやメタデータを同時にストリーミング送信したい場合、自前でマルチパート(multipart/form-data)の仕様に沿ったバイト配列(バウンダリ文字列やヘッダー文字列のバイナリ結合)を組み立てるストリームを自作するという、極めて非現実的な実装が必要になります。
2. サーバー側とインフラ側の高い要求要件
フロントエンドだけでなく、バックエンド側にも以下の特殊な対応が必須となります。
- HTTP/2 または HTTP/3 の接続維持: HTTP/1.1環境では、ブラウザがFetchによるストリーム送信を拒否(制限)するため、インフラ(Nginx、ロードバランサー等)およびサーバーがHTTP/2以降に対応している必要があります。
- アプリケーション側のストリーミング受信実装: サーバー側のプログラムが、流れてくるチャンクを随時パースして消費するストリーム処理で書かれている必要があります。従来の「リクエストボディを一括でメモリや一時ファイルに読み込む」実装のままだと、ストリームが途中で詰まるか送信エラーになります。
3. ブラウザ間の互換性問題
Chromium系 (Chrome, Edgeなど) 以外、特にSafariやFirefoxにおいては依然としてデフォルトでこのストリーミング機能が利用できないか実装に制限があり、クロスブラウザ環境での安定動作が保証されません。
4. 計測される進捗の「信頼性」の乖離
- XHR: ブラウザのネットワーク層が、実際にサーバーへ送信し終えた物理量 (TCPのACK受信ベース) を検知します。
- Fetch: JavaScriptが、ブラウザの送信バッファにデータを引き渡した量しか検知できません。そのため回線が細い環境では「JS上の表示は100% (バッファへの引き渡し完了) なのに、実際の送信はまだ終わっていない」という時間差のズレが生じます。
総合結論
- リクエストのキャンセル・ダウンロード進捗:Fetch API (
AbortController/ReadableStream) で完全に実用的な代替が可能です。 - アップロード進捗:
フロント・バックエンド双方に高度な技術スタックを要求し、かつFormDataも使えなくなるFetch Upload Streamingに比べ、XMLHttpRequestは「HTTP/1.1でも動作し」「サーバー側は従来通りの一括受信でよく」「FormDataで複雑なデータを詰め込んでも、あらゆるブラウザで実際の物理送信量を正確に測れる」という圧倒的な実用性を保ち続けています。
したがって「ファイルアップロードの進捗表示 (特に複数ファイルやテキストパラメータが混在するケース)」が必要なシナリオにおいては、現在でもXMLHttpRequest (あるいは内部でXHRをラップしているAxios等のライブラリ) を選択するのが技術的に正しい判断となります。