ykzts.com

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で進捗を取得するにはbodyReadableStreamを直接渡し、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で進捗を取るためには bodyReadableStream を直接指定しなければなりません。これは単一のバイナリを流すことを意味するため、ブラウザが境界線を自動生成してくれる 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等のライブラリ) を選択するのが技術的に正しい判断となります。