なぜ「ブラウザのスクロール」は、JSで重い処理をしてもカクつかないのか?
フロントエンドの現場で「レンダリングが重い」という相談を受けるとき、多くのエンジニアが真っ先に疑うのは `requestAnimationFrame` のループや、膨大なDOM操作です。確かにそれらはメインスレッドを殺す最大の要因ですが、「なぜスクロールやCSSアニメーションだけは、メインスレッドが死んでいても滑らかに動くのか?」という問いに即答できるでしょうか。
ここを理解しているかどうかで、パフォーマンスチューニングの「打率」は劇的に変わります。今日は、ブラウザの影の立役者「コンポジター・スレッド」の話をしましょう。
—
ブラウザの裏側:メインスレッド vs コンポジター・スレッド
ブラウザは単一の巨大なループではありません。特にモダンなブラウザ(Chromium系など)は、責務を細かく分割しています。
- メインスレッド(Main Thread): JSの実行、DOMの構築(Parse)、CSSの計算(Recalculate Style)、レイアウト(Layout)といった「計算」を担う場所。いわば脳みそです。
- コンポジター・スレッド(Compositor Thread): 画面上の「絵」を統合(Composite)する役割。スクロールイベントを受け取り、GPUに「ここを動かせ」と命令を送る、いわば現場監督です。
なぜこれが重要なのか?
もしスクロールの処理までメインスレッドが握っていたら、ページ内で `while(true)` のような重い計算が走った瞬間にスクロールが停止し、ユーザーは「フリーズした」と感じます。しかし、コンポジターはメインスレッドから「この層(Layer)はここにあるよ」という地図だけ先に受け取っておくため、メインスレッドが死んでいても、コンポジターは「すでに持っている地図」をずらすだけでスクロールを実現できるのです。
これが、「並列処理」が生む魔法の正体です。
—
コンポジターの力を引き出す:`will-change` の功罪
コンポジター・スレッドに処理を任せるには、その要素を「別のレイヤー(Composited Layer)」として独立させる必要があります。昔は `transform: translateZ(0)` という呪文が流行りましたが、今はもっとスマートに書きましょう。
.scroll-optimized-element {
/ ブラウザに「この要素は変化するから、前もって独立したレイヤーにしといてくれ」と伝える /
will-change: transform;
/ レイヤー化を明示的に強制するCSSプロパティ /
/ ただし、使いすぎるとメモリを食いつぶすので注意。必要な場所にだけ貼るのがプロの作法 /
}
注意: 「とりあえず全部に `will-change` を貼れば爆速になる」と勘違いしているジュニアエンジニアがいますが、それは違います。レイヤーが増えるということは、ブラウザがメモリを大量に消費し、GPUへの転送コストも増大するということです。「本当に動きが激しい部分」だけにピンポイントで投与するのが、現場のベストプラクティスです。
—
実践:メインスレッドをブロックしつつ、アニメーションを維持する
実際に、メインスレッドをわざと止めても、コンポジターが動いていれば滑らかさが維持できることを確認する実験的なコードです。
/
- メインスレッドを意図的にブロックする関数
- @param {number} ms – ブロックする時間
/
function blockMainThread(ms) {
const start = Date.now();
while (Date.now() – start < ms) {
// ここでJSが重い処理をしていると仮定
}
}
// 画面上の特定の要素に対して、CSSの transform でアニメーションさせる
const box = document.querySelector('.box');
// ユーザーがボタンを押した瞬間にメインスレッドを5秒間止める
document.querySelector('#trigger').addEventListener('click', () => {
console.log(‘メインスレッドを止めるよ…’);
// これを実行しても、CSSアニメーションは止まりません!
// なぜなら、コンポジター・スレッドが独立してGPUで処理しているから。
blockMainThread(5000);
console.log(‘メインスレッド復帰!’);
});
このコードをブラウザで動かしてボタンを押してみてください。JSの実行中はコンソールに何も表示されず、ブラウザ全体が反応しなくなりますが、CSSで定義されたアニメーションだけは滑らかに動き続けます。これが「コンポジター・スレッドの独立性」です。
—
シニアからのアドバイス:レンダリング・パイプラインを意識せよ
エンジニアとして成長するなら、常に以下の「レンダリング・パイプライン」を頭に浮かべてください。
1. JavaScript: 変更を加える
2. Style: CSSの再計算
3. Layout: どの要素がどこにあるか計算(重い!)
4. Paint: 各ピクセルの描画(重い!)
5. Composite: レイヤーの合成(軽い!)
「Layout」や「Paint」を避けて、直接「Composite」だけで完結するプロパティ(`transform` や `opacity`)を使うこと。 これが、ブラウザのアーキテクチャを理解した人間が書く「高速なUI」の鉄則です。
何かを動かしたいとき、`top` や `left` を操作していませんか? それはパイプラインの最初からやり直しを強いる、最も非効率な手段です。`transform: translate()` を使えば、コンポジター・スレッドがニヤリと笑って、メインスレッドを介さずに高速処理してくれますよ。
フロントエンドのパフォーマンスとは、ブラウザという巨大な機械の「機嫌」を損ねないように、賢く仕事を振る技術のことです。ぜひ、今日から意識してみてください。

コメント