【実務・中級編】 ブラウザのメインスレッドとイベントループ – Webブラウザの仕組み実践ガイド

フロントエンドの現場で「なぜか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);` (書き込み→読み込み→再計算のループ)
  • 良い例: 読み込みは先にまとめて行う。

—

最後に:ブラウザは「協調的なエンジン」である

ブラウザという巨大なエンジンは、あなたのコードを「できるだけ快適に動かそう」と努力している。しかし、我々開発者がメインスレッドにゴミを溜め込んだり、レンダリングの行列に割り込んだりすれば、その努力も水の泡だ。

「このコードがどのタイミングで実行され、ブラウザの描画を待たせているか?」

この問いを常に持ち続けること。そうすれば、あなたの書くコードは驚くほど滑らかに、そしてユーザーにとって心地よいものへと進化するはずだ。現場からは以上だ。次は具体的な計測方法について、また深く掘り下げてみよう。

コメント

タイトルとURLをコピーしました