JavaScriptのEvent LoopからReactのレンダリング仕組みまで
⚠️ 注意: 私の日本語はあまり得意ではないため、AIを使って翻訳しました。わかりにくい点があればご容赦ください🙏
目次
- はじめに
- 第2章:Event Loop / Call Stack / Microtask / Macrotaskの概念
- 第3章:ReactがEvent Loopに「飛び込む」とき — BatchingからFiberまで
- おわりに
1. はじめに
JavaScriptはシングルスレッドの言語でありながら、非同期処理を違和感なくこなします。しかし「なぜそれが可能なのか」を正確に説明できるエンジニアは、意外と多くありません。
Event Loopは、JavaScriptランタイムの根幹をなす実行モデルです。この仕組みを深く理解することは、パフォーマンスのボトルネック特定、予測可能な非同期処理の設計、そしてReactの内部挙動の把握に直結します。
以下の3つの挙動は、いずれもEvent Loopへの理解なしには正確に説明できません。
Why output? // 3 3 3
なぜvarを使ったループの中でsetTimeoutを使うと、0 1 2ではなく3 3 3が出力されるのでしょうか?
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 0);
}
// Output: 3 3 3
UIが「フリーズ」する現象
なぜ重いデータ処理を行う関数を実行すると、ユーザーのPCが32GBのRAMを積んでいても、画面上のボタンがクリックできなくなるのでしょうか?
function heavyTask() {
const start = Date.now();
while (Date.now() - start < 5000) {
// 5秒間ループし続ける
}
console.log('処理完了');
}
document.getElementById('btn').addEventListener('click', heavyTask);
// ボタンをクリックすると、5秒間UIが完全にフリーズする
ReactのStateのわかりにくさ
なぜReactではsetStateを呼び出した直後にconsole.logしても、値が……古いままなのでしょうか?
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
console.log(count); // 1ではなく、まだ0のまま
};
これらはいずれも仕様上の挙動であり、Event Loop・Microtask・Macrotask の実行モデルによって完全に説明できます。
本記事では以下の3点を体系的に解説します:
- Event Loopの内部構造:Call Stack・Web API・Microtask/Macrotask Queueの関係
- ReactとEvent Loopの接点:BatchingやFiberがどのようにEvent Loopを活用しているか
- 実践的な設計指針:メインスレッドのブロッキングを避け、応答性の高いUIを実現する方法
各概念の「なぜ」まで理解することを目標に進めていきます。
第2章:Event Loop / Call Stack / Microtask / Macrotaskの概念
1. Event Loopとは?
Event Loop(イベントループ)とは、JavaScriptが本質的に シングルスレッド(一度に一つの処理しかできない)であるにもかかわらず、APIコールやタイマーなどの非同期タスクを実行できるようにするための調整メカニズムです。
その役割はシンプルです:Call StackとCallback Queueを継続的に監視し、Call Stackが空になったらキューからタスクを取り出してStackに積んで実行します。
2. なぜEvent Loopが重要なのか?
Event Loopを理解することは、単に面接対策のためだけでなく、効率的なコードを書くためのものです:
- アプリのフリーズを防ぐ: メインスレッドをブロックせずに非同期タスクを処理できる
- スムーズな非同期処理: APIコールやデータ操作を行っても、他の処理を止めない
- 安全な実行: JSはシングルスレッドなので、タスクの調整・管理によりリソースの競合を防ぐ
- PromisesとAsync/Awaitのサポート: 「コールバック地獄」に陥らず、コードをシンプルに保てる
- 実行フローの制御: コードの実行順序を予測できるため、デバッグが容易になる
- スムーズなUI維持: バックグラウンドの重い処理があっても、ユーザー体験が中断されない
- パフォーマンス向上: 複数スレッドを作らずに多くのタスクを効率的に処理でき、システムリソースを節約できる
3. Event Loopの動作メカニズム
3.1. Call Stack(コールスタック)
Call Stackは、関数の実行を管理するスタック型データ構造です。
- LIFO(Last In, First Out:後入れ先出し)方式で動作
- 関数が呼び出されると、スタックにPushされる
- 関数が完了または
return文に到達すると、スタックからPopされる
3.2. Web API
JSエンジンの外側(ブラウザが提供)にある「助っ人」たちです。setTimeout、fetch、DOM Eventsを呼び出すと、JSはそれらをWeb APIに委ねてCall Stackを解放します。処理が完了すると(例:2秒待った後)、Web APIはコールバックをキューに追加します。
3.3. Microtask(優先度の高いキュー)
Promise.then()、catch()、MutationObserverなどが含まれます。
- 優先度: 最高
- Event LoopはCall Stackが空になった直後、すべてのMicrotaskキューのタスクを実行してから、Macrotaskへ移行します
3.4. Macrotask(通常のキュー)
setTimeout、setInterval、setImmediate、I/Oイベントなどが含まれます。
- Microtaskキューが空になった後に実行されます
3.5. 実行フロー(Execution Flow)の例
以下のコードで優先度の違いを見てみましょう:
console.log("1. 同期処理 - 開始"); // (A)
setTimeout(() => {
console.log("2. Macrotask - setTimeout"); // (B)
}, 0);
Promise.resolve().then(() => {
console.log("3. Microtask - Promise"); // (C)
});
console.log("4. 同期処理 - 終了"); // (D)
実行フローの解説:
-
ステップ1:
console.log("1...")(A) がCall Stackに積まれ、即座に出力される -
ステップ2:
setTimeoutが呼ばれ、コールバック(B)がWeb APIに送られる。待機時間が0msなので、Web APIはすぐに(B)をMacrotask Queueに追加する -
ステップ3:
Promise.resolve()がコールバック(C)を生成し、Microtask Queueに追加される -
ステップ4:
console.log("4...")(D) がCall Stackに積まれ、出力される -
ステップ5: Call Stackが空になる。Event LoopはまずMicrotask Queueを確認。(C)を見つけてStackに積み、出力される:
3. Microtask - Promise -
ステップ6: Microtask Queueが空になる。Event LoopはMacrotask Queueを確認。(B)をStackに積み、出力される:
2. Macrotask - setTimeout
最終出力:
1. 同期処理 - 開始
4. 同期処理 - 終了
3. Microtask - Promise
2. Macrotask - setTimeout
第3章:ReactがEvent Loopに「飛び込む」とき — BatchingからFiberまで
Event LoopがJavaScriptの「鼓動」だとすれば、Reactはその鼓動に合わせて自分の呼吸を調整できる「アスリート」のようなものです。この章では、ReactがMicrotasksをどう活用し、「分割して統治する」仕組みでパフォーマンスを最適化しているかを探っていきます。
3.1. JSロジックとUIレンダリングの戦い
ブラウザでは、JavaScriptの実行と画面の描画(Repaint/Reflow)が同一のメインスレッドを共有しています。
- 16.6msのルール: 60fpsを達成するために、ブラウザは1サイクルを16.6ms以内に完了する必要があります。JSコード(Call Stack)が長時間実行され続けると、ブラウザが画面を再描画できなくなり、Jank(カクつき) が発生します
- Reactの介入: Reactはメインスレッドを長時間占有しないよう設計されており、ブラウザが常にUIを更新できる余地を確保しています
3.2. Batchingのメカニズム:「まとめ処理」の技術
ReactはsetStateを呼び出してもすぐにUIを再レンダリングしません。代わりに、複数の更新を「まとめて」処理します。
- 仕組み: Reactはイベントハンドラー内の同期コードがすべて実行し終わるのを待ちます。その後、まとめた更新処理を1つのMicrotaskとして実行します。これにより、余分な再レンダリング回数を削減できます
-
Automatic Batching(React 18+): 以前(v17)はReactのイベント内でのみBatchingが機能していましたが、v18からは
setTimeout、fetch、ネイティブイベント内でsetStateを呼び出しても、Reactが自動的に1つのMicrotaskにまとめて処理するようになりました
3.3. Deep Dive:React Fiber — 「分割して統治する」解決策
以前(Stack Reconciler)のReactは、コンポーネントツリーを最初から最後まで一気にレンダリングしていました。ツリーが大きくなるとCall Stackが詰まる問題がありました。React Fiberはそのルールを変えるために生まれました:
- 一時停止と再開(Pause & Resume): Fiberは作業を小さな「作業単位」に分割します。各単位の後、ReactはEvent Loopを確認します。高優先度のタスク(クリック、キー入力)があれば、レンダリングを一時停止してブラウザにそのインタラクションを先に処理させ、その後で再開します
- Concurrency(並行処理): 複数バージョンのUIを同時に準備できるようになり、重いデータのレンダリング中でもアプリがユーザー操作にすぐ応答できるようになります
3.4. Scheduler:優先度管理システム
ReactはSchedulerという内部パッケージを持ち、緊急度に基づいて作業キューを管理しています:
- ImmediatePriority(即時優先): 即座に完了する必要がある処理(入力、クリック)
- UserBlockingPriority(ユーザーブロッキング優先): 目に見えるフィードバック(アニメーション)
- NormalPriority(通常優先): 通常のタスク(データのフェッチ)
- IdlePriority(アイドル優先): ブラウザの空き時間に実行する処理(ログ送信、クリーンアップ)
3.5. パフォーマンス最適化の実践
Event Loopを理解することで、React 18の「武器」を正確に使いこなせるようになります:
- メインスレッドのブロックを避ける: 重い処理(大量の計算)をメインスレッドで直接実行しないようにしましょう。タスクを細かく分割するか、Web Workerに委譲することを検討してください
-
useTransitionを活用する: あるstateの更新を「低優先度(二次的)」としてマークできます。Reactはユーザーインタラクションを優先し、必要に応じてこのstateのレンダリングをキャンセル・一時停止できます -
useDeferredValueを活用する: 重要度の低い値の更新(例:ユーザーが入力中の検索結果)を遅延させ、フレームレートを安定させます
おわりに
Event Loopを理解することは、なぜsetTimeoutがPromiseより後に実行されるかを解明するだけでなく、React Fiberの「魂」を深く理解することにもつながります。
Call Stack、Microtasks、Macrotasksを巧みに調整することで、Reactはシングルスレッドという「不器用な」JavaScriptを、複雑なWebアプリケーションにも対応できる強力なツールへと変えました。それでいながら、ユーザーにスムーズな体験を届け続けています。
Happy Coding!
**

