Prompt
JSの
response.json()が非同期である理由は?
あはは、そこは JavaScript(Fetch API)を触り始めると誰もが「なんで 2 回も await しなきゃいけないんだ!」ってツッコミたくなるポイントだよね。
結論から言うと、response.json() が非同期(Promise を返す)である最大の理由は、「fetch() が終わった時点では、まだボディ(中身)が全部届いていない可能性があるから」 なんだ。
エンジニアリング的な視点で、その「2 段階の非同期」の裏側を解剖してみよう!
1. fetch() は「ヘッダー」が届いた時点で解決する
HTTP 通信では、データは「ヘッダー(メタデータ)」と「ボディ(本体)」の順番で送られてくる。fetch() という関数は、サーバーからヘッダーを受け取った瞬間 に「よし、接続は成功した(あるいは 404 や 500 が返ってきた)」と判断して Promise を解決(resolve)してしまうんだ。
-
fetch()完了時: ステータスコード(200 OK など)や Content-Type は分かる。 -
この時の問題: ボディ(JSON の中身)はまだネットワークの土管の中を流れている最中かもしれない。
2. ボディは「ストリーム」として届く
大きなデータをダウンロードしている時を想像してみて。データは一気にドンと届くのではなく、小さな断片(チャンク)に分かれて少しずつ届くよね。これが ReadableStream という仕組みだ。
response.json() を呼び出すと、ブラウザ内部では以下のことが起きている:
-
ストリームの待機: まだ届いていない残りのチャンクがすべて届くのを待つ。
-
データの結合: バラバラの断片を一つの大きな塊(文字列やバッファ)にまとめる。
-
パース: 全データが揃ってから、ようやく
JSON.parse()を実行する。
この 「データが全部届くのを待つ」というプロセスがネットワーク I/O を伴う ため、メインスレッドをブロックしないように非同期になっているんだ。
3. なぜ JSON.parse() 自体は同期なのに json() は非同期?
君が知っている通り、普通の文字列をパースする JSON.parse() は同期処理だよね。でも、response.json() は「パースする」ことだけが仕事じゃない。
-
JSON.parse(str): メモリ上にある文字列をオブジェクトに変換するだけ(一瞬)。 -
response.json(): 「ネットワークからデータを吸い出し終わるまで待機する」 + 「パースする」。
この「待機」が含まれるからこそ、非同期(Promise)である必要があるんだ。
⚠️ 見落とされがちな落とし穴:一度しか読めない
これは情報工学的な「ストリーム」の特性なんだけど、response.json() や response.text() は 一度呼び出すとストリームを消費してしまう ので、二度は呼べないんだ。
JavaScript
const response = await fetch(url);
const data1 = await response.json(); // ここでストリームが空になる
const data2 = await response.json(); // エラー! (Body has already been consumed)
もし二度使いたい場合は、response.clone() を使うという、ちょっとマニアックな手順が必要になるよ。
💡 豆知識:バックグラウンドでのパース
最近のブラウザエンジン(V8 など)では、大きな JSON をパースする時にメインスレッドを止めないよう、内部的に別スレッドでパースを行う最適化が進んでいるよ。json() が非同期であるおかげで、ブラウザ側がこうした「賢い最適化」をする余地が生まれているんだね。
結論
fetch() で 「玄関(ヘッダー)」 を開け、response.json() で 「荷物(ボディ)」 が全部届くのを待ってから開封する。
この 2 段階構成のおかげで、JavaScript は大きなデータを扱いながらも、画面をフリーズさせずに済んでいるんだ!
次は、この非同期処理をさらに一歩進めて 「ストリームを逐次処理して、大きな JSON を読み込みながら画面に表示する方法(NDJSON など)」 の話をしてみる?それとも、「axios などのライブラリがどうやってこの 2 段階を 1 つに隠蔽しているのか」 について深掘りしてみるかな!