フロントエンドの現場で「なぜかUIがカクつく」「意図したタイミングでDOMが更新されない」といった壁にぶつかったことはないだろうか?
多くのエンジニアが「JavaScriptはシングルスレッド」と暗記しているが、その実態である「メインスレッドの消化試合」のルールを正確に理解している者は意外と少ない。今日は、ブラウザの心臓部であるイベントループとレンダリングパイプラインの深淵について、現場の知見を交えて解き明かしていく。
—
1. 脳内で描くべき「メインスレッドの処理順序」
ブラウザのメインスレッドは、常に以下のループを高速で繰り返している。この順番こそが、パフォーマンスの全てを握っていると言っても過言ではない。
1. タスクキュー (マクロタスク): `setTimeout`, `setInterval`, `I/O`, UIイベントなど。
2. マイクロタスクキュー: `Promise.then`, `MutationObserver`, `queueMicrotask`。
3. レンダリングパイプライン: `requestAnimationFrame(rAF)` → スタイル計算 → レイアウト → ペイント。
ここでの鉄則は、「マイクロタスクはタスクの直後に、全消化されるまでループを抜けない」ということだ。つまり、非同期処理を詰め込みすぎると、メインスレッドがロックされ、レンダリングが後回しにされる。これが「UIフリーズ」の正体だ。
—
2. requestAnimationFrame(rAF) という「魔法の杖」
多くのエンジニアが `setTimeout(fn, 0)` を使って「次のフレームで処理を動かそう」と試みるが、これはおすすめしない。`setTimeout` はブラウザの描画タイミングとは無関係にタスクキューに放り込まれるため、画面のリフレッシュレートとズレが生じ、結果として「ちらつき」や「カクつき」を招く。
`rAF` は、ブラウザが「これから画面を書き換えるよ」という直前のタイミングで呼び出されるコールバックだ。DOM操作を伴うアニメーションやレイアウト変更を行うなら、これを使わない手はない。
実践:重い処理を分割してUIを殺さないコード
DOMを大量に書き換える必要がある際、同期的に処理するとメインスレッドが数ミリ秒間完全停止する。これを回避する「タスク分割」のパターンを見てほしい。
/
- 大量のDOM操作を分割して実行するユーティリティ
- メインスレッドを占有せず、ユーザーの操作を阻害しないための工夫
/
async function processLargeData(items) {
const CHUNK_SIZE = 100; // 一度に処理する件数
for (let i = 0; i < items.length; i += CHUNK_SIZE) { // 処理を次のブラウザ描画サイクルまで遅延させる await new Promise(resolve => requestAnimationFrame(resolve));
const chunk = items.slice(i, i + CHUNK_SIZE);
chunk.forEach(item => {
const div = document.createElement(‘div’);
div.textContent = item.name;
document.body.appendChild(div);
});
console.log(`残り: ${items.length – (i + CHUNK_SIZE)} 件`);
}
}
// 使い方: 大規模リストのレンダリング時にUIを固めない
const mockData = Array.from({ length: 1000 }, (_, i) => ({ name: `アイテム ${i}` }));
processLargeData(mockData);
—
3. なぜ「マイクロタスク」を乱用してはいけないのか
`Promise` の多用は非常に便利だが、マイクロタスクは「レンダリングよりも優先される」という特性を持つ。
仮に、ループの中で無限に `Promise.resolve().then(…)` を生成し続けるようなコードを書くと、ブラウザはレンダリングパイプラインに到達できなくなる。結果、ユーザーの画面は完全にフリーズする。
現場でよくあるバグが、「非同期処理の中でDOMを更新しすぎて、画面が更新されないまま数秒経過する」というケースだ。重い計算が必要な場合は、メインスレッドを解放するために `Web Workers` を使うか、上記のように `rAF` で意図的にレンダリングの隙間を作ることが不可欠だ。
—
4. プロの現場でのデバッグTips
ブラウザの「パフォーマンスパネル」を見る際、以下の点に注目してほしい。
- Long Tasks (赤いバー): 50msを超える処理は、ユーザーがカクつきを感じる閾値だ。ここを削るのが我々の腕の見せ所。
- Layout Thrashing: JavaScriptで「DOMの読み込み」と「書き込み」を交互に繰り返すと、ブラウザはスタイル計算を強制的にやり直す。
- 悪い例: `div.style.width = ‘100px’; console.log(div.offsetWidth);` (書き込み→読み込み→再計算のループ)
- 良い例: 読み込みは先にまとめて行う。
—
最後に:ブラウザは「協調的なエンジン」である
ブラウザという巨大なエンジンは、あなたのコードを「できるだけ快適に動かそう」と努力している。しかし、我々開発者がメインスレッドにゴミを溜め込んだり、レンダリングの行列に割り込んだりすれば、その努力も水の泡だ。
「このコードがどのタイミングで実行され、ブラウザの描画を待たせているか?」
この問いを常に持ち続けること。そうすれば、あなたの書くコードは驚くほど滑らかに、そしてユーザーにとって心地よいものへと進化するはずだ。現場からは以上だ。次は具体的な計測方法について、また深く掘り下げてみよう。

コメント