# XMLHttpRequestとFetch APIの機能差および移行における制約の整理

Web標準における非同期通信の主流はXMLHttpRequest (以下、XHR) からFetch APIへと移行しましたが、ユースケースによっては現在でもXHRを選択せざるを得ない領域が存在します。本稿では主要な3つの機能差 (キャンセル、ダウンロード進捗、アップロード進捗) について、最新の仕様および実用上の制約を踏まえて比較と解説します。

## 各機能の詳細と実装比較

### リクエストのキャンセル

Fetch APIの登場当初はリクエストの中断手段がありませんでしたが、現在は**`AbortController`**の導入により、XHRの`xhr.abort()`と同等以上の柔軟な制御が可能です。

#### XHRによる実装

```javascript
const xhr = new XMLHttpRequest();  
xhr.open('GET', '/path/to/data');  
xhr.send();  
  
// 任意のタイミングで中断  
xhr.abort();
```

#### Fetch APIによる実装

```javascript
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による実装

```javascript
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による実装

```javascript
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つのイベントで正確に自動計測してくれます。

```javascript
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側でチャンクの消費量をカウントする必要があります。

```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等のライブラリ) を選択するのが技術的に正しい判断となります。
