やあ、よく来てくれた。
現場でバリバリとコードを書いていると、一度はこんな謎にぶつかったことがあるはずだ。
「あれ、このコード、非同期処理のつもりで書いたのに、なんで画面の描画が盛大にカクつくんだ?」
「`Promise` と `setTimeout` って、どっちが先に実行されるんだっけ……いや、待てよ、DOMの更新はどのタイミングで挟まってくるんだ?」
画面がバグる、カクつく、ユーザーからの入力に対してインタラクションがワンテンポ遅れる。こうしたフロントエンドの「説明のつかない不具合」の9割は、今日話す「レンダリングエンジンとイベントループの統合」の仕組みを腹落ちさせていないことが原因だ。
教科書や仕様書のスペックをただ暗記しても、実務の現場では何の役にも立たない。
今日は、Webブラウザの内部で何が起きているのか、その泥臭いまでのリアルな仕組みを、シニアの視点から徹底的に解剖してやろう。
—
1. そもそもブラウザの「イベントループ」とは何か?
JavaScriptはシングルスレッドだ。これはもう常識だな。
だが、考えてみてほしい。シングルスレッドなのに、なんで裏でタイマーが動き、ネットワークリクエストを待ち受け、ユーザーがマウスをグリグリ動かした瞬間にイベントハンドラが発火するのか?
その答えが、ブラウザのイベントループ(Event Loop)だ。
正確に言うと、JavaScriptのエンジン(V8など)単体にはイベントループなんてものは存在しない。あれはブラウザという巨大なコンテナ(宿主環境)が提供する仕組みなのだ。
イベントループの心臓部は、主に以下の3つの要素で構成されている。
1. コールスタック(Call Stack): 今まさに実行中の関数が積まれる場所。
2. タスクキュー(Task Queue / MacroTask Queue): `setTimeout` や `setInterval`、DOMイベントなどの「重い」仕事が待機する列。
3. マイクロタスクキュー(MicroTask Queue): `Promise.then` や `MutationObserver`、`queueMicroTask` などの「一刻も早く片付けたい超特急の仕事」が待機する列。
そして実務で最も重要なのが、これらと「レンダリング更新フェーズ(Rendering Update Phase)」がどう絡み合っているかだ。ここを誤解していると、無駄な再描画(レイアウトスラッシング)の嵐を引き起こすことになる。
—
2. タスク・マイクロタスク・レンダリングの「優先順位の掟」
ブラウザが裏側で回しているイベントループの1サイクル(Tick)を、エンジニアの言葉でリアルに分解してみよう。ブラウザは常に以下のループを狂ったように高速(通常は60fps=約16.6msごと)で回している。
1サイクルの流れ
1. マクロタスクの実行: タスクキューから1つだけタスクを取り出して実行する。
2. マイクロタスクの全消化(最重要): コールスタックが空になった瞬間、ブラウザはマイクロタスクキューが空になるまで、すべてを一気に実行し尽くす。ここがポイントだ。マイクロタスク実行中に新たなマイクロタスクが生まれれば、それも容赦なくこのタイミングで消化される(無限ループに陥る危険性もあるので注意しろ)。
3. レンダリングの判断: ブラウザは「今、画面を更新すべきタイミングか?」を内部のフラグやリフレッシュレート(60Hzなど)に合わせて判断する。
- 更新タイミングであれば、requestAnimationFrame (rAF) のコールバック実行 $\rightarrow$ スタイル計算 (Style) $\rightarrow$ レイアウト (Layout / Reflow) $\rightarrow$ ペイント (Paint) の順に処理を走らせる。
ここで多くのエンジニアが勘違いする。「`Promise` が解決されたら即座に画面が更新される」わけではない。画面が更新されるのは、あくまでマイクロタスクがすべて終わった後の、レンダリングフェーズなのだ。
—
3. 実務で差が出る! イベントループをハックするコード例
百聞は一見にしかずだ。以下のコードを見てほしい。
「コンソールに何番の順番でログが出るか」、そして「画面の描画はどのタイミングで挟まるか」を頭の中でトレースできるか?
// 実務で頻出するイベントループの挙動確認用コード
console.log(‘1. 同期処理: スクリプト開始’);
// 1. マクロタスクを登録
setTimeout(() => {
console.log(‘5. setTimeout (マクロタスク)’);
}, 0);
// 2. マイクロタスクを登録
Promise.resolve().then(() => {
console.log(‘3. Promise.then (マイクロタスク)’);
// マイクロタスクの最中にさらにマイクロタスクを追加
queueMicroTask(() => {
console.log(‘4. ネストされたマイクロタスク’);
});
});
// 3. requestAnimationFrameを登録
requestAnimationFrame(() => {
console.log(‘6. requestAnimationFrame (レンダリング前)’);
});
console.log(‘2. 同期処理: スクリプト終了’);
実行結果の正解
1. 同期処理: スクリプト開始
2. 同期処理: スクリプト終了
3. Promise.then (マイクロタスク)
4. ネストされたマイクロタスク
5. setTimeout (マクロタスク)
— (ここで画面のレンダリング・rAFが挟まる) —
6. requestAnimationFrame (レンダリング前)
この挙動を綺麗に説明できれば、君のチームのジュニアや中堅メンバーからの信頼は爆上がりするはずだ。
`setTimeout`(マクロタスク)よりも、`Promise`(マイクロタスク)のほうが先に実行される。そして、マクロタスクやマイクロタスクがすべて片付いたあとに初めて、ブラウザは「よし、画面を塗り替えるか」とレンダリングフェーズに移行するのだ。
—
4. 現場の罠:レイアウトスラッシングと `requestAnimationFrame` の正しい使い方
さて、ここからがシニアとしての本当の腕の見せ所だ。
「DOMを書き換えて、すぐにその高さを取得したい」という要件があったとする。これを何も考えずに書くと、イベントループとレンダリングの仕組みを無視した最悪のコードができあがる。
❌ やってはいけないアンチパターン
// 悪い例:DOMの読み書きが混在し、レイアウトスラッシングを引き起こす
const box = document.getElementById(‘my-box’);
// ループ内でDOMの読み取りと書き込みを交互に行う
for (let i = 0; i < 100; i++) {
// 書き込み(スタイル変更 -> レイアウト無効化)
box.style.width = (box.offsetWidth + 10) + ‘px’;
// 読み取り(強制的にレイアウト(Reflow)を発生させる!)
console.log(box.offsetWidth);
}
このコードは、ブラウザを強制的に何度も再計算(Reflow)させ、メインスレッドを完全に殺す。これがレイアウトスラッシングだ。
⭕ ベストプラクティス:rAFを活用した処理の分離
ブラウザのレンダリングフェーズを味方につけ、読み取りと書き込みを綺麗に分離するには `requestAnimationFrame` を使う。
// 良い例:rAFを使ってブラウザのレンダリングサイクルに同期させる
const box = document.getElementById(‘my-box’);
function optimizeAnimation() {
// 1. まず「読み取り」をまとめて行う
const currentWidth = box.offsetWidth;
// 2. 次に「書き込み」をまとめて行う(ブラウザのレイアウト計算を1回に抑制)
requestAnimationFrame(() => {
box.style.width = (currentWidth + 10) + ‘px’;
});
}
// ユーザーのアクションやイベントから呼び出す
window.addEventListener(‘resize’, () => {
optimizeAnimation();
});
`requestAnimationFrame` は、「ブラウザが次に画面を描画する直前」にコールバックを実行してくれる。これを使うことで、無駄なレイアウト計算を省き、60fps(あるいはそれ以上)の滑らかなUI体験を担保できるのだ。
—
5. まとめ:シニアエンジニアからのメッセージ
Webブラウザのレンダリングエンジンとイベントループの統合を理解するということは、「ブラウザという優秀だけど気難しい相棒と、同じ言語で会話できるようになる」ということだ。
- ユーザー入力を阻害する重い処理は、タスクキューへ逃がす(あるいはWeb Workerを使う)。
- 状態変化に伴う細かなデータの確定はマイクロタスク(`Promise`)で素早く片付ける。
- 視覚的なDOMの更新は、ブラウザのレンダリングサイクル(`requestAnimationFrame`)に委ねる。
この3つの原則を頭に叩き込んでおけば、どんなに複雑なシングルページアプリケーション(SPA)を構築しようとも、カクつかない滑らかなUIを作り上げることができる。
明日からのコードレビューでは、「なぜここに `setTimeout` を使っているのか?」「このDOM操作で強制Reflowが起きていないか?」という視点を持って、コードを見つめ直してみてほしい。
君の書くコードのクオリティが、一段も二段も跳ね上がるはずだ。それじゃあ、また現場で会おう。

コメント