【実務・中級編】 メインスレッドの役割と制約 – Webブラウザの仕組み実践ガイド

よう、調子はどうだい?現場でゴリゴリとコードを書き進めている君なら、一度は「なぜかスクロールがカクつく」「ボタンを押した瞬間に画面が固まる」といった、あの忌々しい「ジャンク(Jank)」に頭を抱えたことがあるはずだ。

今日は、我々フロントエンドエンジニアが一生付き合っていくことになる「メインスレッド」という名の孤独な働き者について、その裏側で何が起きているのか、どうすれば彼を過労死させずに済むのかを深く掘り下げていこう。

マニュアルをなぞっただけの知識は捨ててくれ。ブラウザの内部構造という名の「戦場」で、いかにしてスムーズなユーザー体験(UX)を勝ち取るか、その真髄を伝授するよ。

—

1. メインスレッド:あまりにも多すぎる責任

Webブラウザのレンダリングプロセスにおいて、メインスレッドはまさに「万事屋」だ。君が書いたJavaScriptを実行し、CSSを解析してスタイルを計算し、要素の大きさを決めて(レイアウト)、色を塗る(ペイント)。これらすべてを、彼はたった一人でこなしている。

ここで重要なのは、「メインスレッドは一度に一つのことしかできない」という冷徹な事実だ。

JavaScriptが巨大なループ処理で100ms占有してしまえば、その間ブラウザは「次のフレーム」を描画することができない。ユーザーがスクロールしようとしても、ボタンをクリックしても、メインスレッドがJavaScriptにかかり切りなら、画面はピクリとも動かない。これが「ブロッキング」の正体だ。

レンダリングパイプラインの工程

1. JavaScript: アニメーションのトリガーやDOM操作。
2. Style (Recalculate Style): どのCSSルールがどの要素に当たるかを計算。
3. Layout (Reflow): 要素の幾何学的な形状(位置とサイズ)を計算。
4. Paint (Repaint): テキスト、色、画像、境界線などをピクセルとして描き出す。
5. Composite (合成): 描画された複数の層(レイヤー)を合成して画面に出す。

このうち、JavaScriptからPaintまでをメインスレッドが担当している。 最後の「Composite」だけが、別のスレッド(コンポジタースレッド)で行われる特別な工程なんだ。

—

2. 「リフロー」という名の悪魔

実務で最も警戒すべきは、リフロー(Layout)だ。
例えば、JavaScriptで要素の `width` や `height` を変更したとしよう。ブラウザは「一つの要素が変わったなら、他の要素の位置も変わるかもしれない」と考え、ページ全体のレイアウトを再計算しようとする。

さらに最悪なのが、「強制同期レイアウト(Forced Synchronous Layout)」だ。

// 現場でよく見かける「死のコード」
const box = document.getElementById(‘box’);

// 1. スタイルを変更する(書き込み)
box.style.width = ‘200px’;

// 2. その直後に現在のオフセット幅を取得する(読み取り)
// ここでブラウザは「まだレイアウト計算が終わってないけど、値を返さなきゃ!」
// とパニックになり、その場で強制的にリフローを実行する。
const width = box.offsetWidth;

console.log(width);

このように「書き込み」の直後に「読み取り」を行うと、メインスレッドは本来のレンダリングスケジュールを無視して計算を強制される。これがループの中で起きれば、パフォーマンスは瞬く間に崩壊する。

—

3. 実践:メインスレッドを解放するための3つの戦術

さて、理屈はわかった。では現場でどう書くべきか。具体的なコードを見ていこう。

戦術A:`requestAnimationFrame` で描画のタイミングを合わせる

`setTimeout` や `setInterval` でアニメーションを作ってはいけない。あれらはメインスレッドの都合を無視して割り込むからだ。`requestAnimationFrame` (rAF) は、ブラウザが「次の描画準備ができる直前」に実行することを保証してくれる。

/

  • 要素をスムーズに横移動させる関数

/
function smoothMove(element, distance) {
let start = null;

function step(timestamp) {
if (!start) start = timestamp;
const progress = timestamp – start;

// 進行状況に合わせて移動距離を計算(例:1msごとに0.1px)
const x = Math.min(progress 0.1, distance);

// transformを使うことで「Composite」のみで処理を完結させる(後述)
element.style.transform = `translateX(${x}px)`;

if (x < distance) { // 次のフレームの描画前に再度自分を呼ぶ window.requestAnimationFrame(step); } } window.requestAnimationFrame(step); } // 実行 const target = document.querySelector('.ball'); smoothMove(target, 500);

戦術B:重い処理は「Web Worker」へ丸投げする

メインスレッドがJavaScriptで忙しいなら、JavaScriptを別のスレッドで動かせばいい。それが Web Worker だ。データの加工や複雑な計算は、彼に任せてメインスレッドは描画に専念させよう。

// main.js
const worker = new Worker(‘worker.js’);

// 重い計算を依頼
worker.postMessage({ data: bigDataArray });

// 結果を受け取る
worker.onmessage = function(e) {
console.log(‘計算完了!結果:’, e.data);
// ここで初めてDOMを更新する
};

// worker.js (別ファイル)
self.onmessage = function(e) {
// ここはメインスレッドをブロックしない「別世界」
const result = heavyCalculation(e.data);
self.postMessage(result);
};

戦術C:`requestIdleCallback` で「暇な時」を狙う

ログの送信や、緊急性の低いデータの事前取得などは、ユーザーが操作していない「メインスレッドの空き時間」にやらせるのがプロの仕事だ。

// ユーザーの操作を妨げないように、アイドル時間に実行
window.requestIdleCallback((deadline) => {
// deadline.timeRemaining() で、次のフレームまであと何ミリ秒余裕があるか分かる
while (deadline.timeRemaining() > 0 && tasks.length > 0) {
processTask(tasks.pop());
}
}, { timeout: 2000 }); // 最大2秒待っても暇にならなければ強制実行

—

4. 究極の回避策:コンポジタースレッドへのバイパス

最後に、チーフアーキテクトとして最も伝えたい「裏技」を教えよう。
それは、「メインスレッドを一切通らずに描画を更新する」ことだ。

先ほど、レンダリングパイプラインの最後に「Composite(合成)」があると言った。実は、`transform` と `opacity` の2つのプロパティだけは、LayoutやPaintをスキップして、GPU(コンポジタースレッド)だけで処理できるという特権を持っている。

  • `top / left` を変える → Layoutが発生する(激重)
  • `width / height` を変える → Layoutが発生する(激重)
  • `transform: translateX()` を変える → Compositeのみ(爆速)

現場でアニメーションを実装する際は、何が何でも `transform` で代用できないか検討してくれ。これだけで、メインスレッドの負荷は劇的に下がる。

—

終わりに

メインスレッドを理解することは、ブラウザという巨大なオーケストラの指揮者になることと同じだ。
誰がどのタイミングで楽器を鳴らすべきか、誰を休ませるべきか。それをコントロールできるようになった時、君が作るWebサイトは、まるでネイティブアプリのような滑らかさを手に入れるだろう。

「コードが動く」のは当たり前だ。その裏でメインスレッドがどう呼吸しているか、常に意識してみてくれ。

次は、メモリリークと戦う「ガベージコレクションの深淵」について話すとしようか。また会おう。

コメント

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