おい、最近フロントエンドのパフォーマンスチューニングでこんな罠にハマってないか?
「CSSの `transform` と `opacity` だけ使ってるからサクサク動くはずなのに、なぜか低スペック端末や画面いっぱいのリッチなUIでスクロールがカクつく……」
もし君がDOMツリーやCSSOMツリーの構築、あるいはメインスレッドでのJavaScriptの実行速度ばかりに気を取られているなら、ブラウザの「裏側の現実」を見落としている可能性が高い。現代のWebブラウザは、メインスレッドがJavaScriptのパースやレイアウト計算でどれだけ悲鳴を上げていようとも、ユーザーのスクロールやピンチイン・アウトに対して60fps(あるいは120fps)を死守するための最終防衛ラインを持っている。
それが今回解説する「コンポジタースレッド(Compositor Thread)」だ。
今回は、このコンポジタースレッドが裏側でどう動き、なぜ私たちの書いたコードがGPUを唸らせるのか、そのアーキテクチャの核心をシニアの視点から徹底的に紐解いていこう。
—
1. なぜメインスレッドだけではWebは生き残れないのか?
まずは、ブラウザの内部構造を少し思い出してほしい。
Webブラウザ(ChromiumやGeckoなど)の心臓部であるメインスレッド(Main Thread)は、文字通り何でも屋だ。
- HTMLのパースとDOMツリーの構築
- CSSのパースとCSSOMツリーの構築、およびスタイル計算(Recalculate Style)
- 要素のジオメトリ(位置やサイズ)を計算するレイアウト(Layout / Reflow)
- JavaScriptの実行(イベントハンドラ、API通信、フレームワークのレンダリングループ)
……どう考えても過重労働だ。もしユーザーがスクロールした瞬間に、メインスレッド上で重いJavaScript(例えば、巨大な配列のソートや、無限スクロールのデータ処理)が走っていたらどうなる?
当然、メインスレッドはブロックされ、画面の描画更新がストップする。これが、かつてのWebが「カクカクして使い物にならない」と言われた根本原因だ。
救世主「コンポジタースレッド」の登場
この悲劇を回避するためにブラウザのアーキテクトたちが編み出したのが、メインスレッドとは完全に独立した別スレッドで動作する「コンポジタースレッド」の存在だ。
コンポジタースレッドの使命はシンプルかつ極限に重い。
「メインスレッドが何をしていようとも、ユーザーのスクロールやピンチ操作には即座に反応し、画面を滑らかに動かし続けること」
これを実現するために、コンポジタースレッドは次のようなウルトラCをやってのける。
1. 入力イベントの先読み: ユーザーが画面に触れた(Touch / Wheel)というイベントを、メインスレッドよりも先にコンポジタースレッドが直接キャッチする。
2. レイヤー分割(Tiling): ページ全体を小さな「レイヤー(Tile)」という画像(ビットマップ)の集まりに分割して保持する。
3. GPUへの描画指示(Compositing): メインスレッドを介さず、スクロールや簡単なアニメーションに伴うレイヤーの位置移動(オフセット)を直接GPUに指示し、合成(コンポジット)して画面に映し出す。
つまり、ユーザーがスクロールしている最中、ブラウザはメインスレッドをバイパスして、コンポジタースレッドとGPUだけで画面をグリグリ動かしている瞬間があるんだ。これが「レイヤー合成の魔法」の正体だ。
—
2. 賢くサボる:ラスタライズと「タイリング」の仕組み
ここで一つ疑問が湧くはずだ。「画面をスクロールしたら、見えていなかった新しい領域を描画しなきゃいけないよね? メインスレッドを通さずにどうやってそれをやるの?」と。
ここに、ブラウザの凄まじい最適化技術が隠されている。
コンポジタースレッドは、ページ全体を「レイヤー」に切り分け、さらにそれを一定サイズ(通常256×256ピクセルなど)の「タイル(Tile)」という単位に細かく分割している。
1. 記録(Painting): まずメインスレッドが、「このレイヤーにはこういう描画コマンド(ペイント記録)がある」という情報をコンポジタースレッドに渡す。
2. ラスタライズ(Rasterization): コンポジタースレッド(あるいはその配下のラスタースレッド群)が、その描画コマンドを解釈して、実際のピクセルデータ(ビットマップ)へと変換する。
3. 賢い先読み(Scroll Prediction): ユーザーが下にスクロールし始めると、コンポジタースレッドは「このペースだと次にこのタイルが必要になるな」と予測し、画面外のタイルをあらかじめラスタライズしてGPUのVRAMに送り込んでおく。
結果として、ユーザーが実際にスクロールしたとき、GPUにはすでに描画済みのタイルが揃っているため、ただ「画像をずらす(Translate)」だけで済む。だから、どれだけDOMが複雑であっても、スクロールだけは驚異的な滑らかさを維持できるわけだ。
—
3. 現場で使える!コンポジタースレッドを味方につける実装パターン
さて、ここからが実務の話だ。
このコンポジタースレッドの恩恵を100%受けるためには、「メインスレッドを巻き込まずに、コンポジタースレッドだけで処理が完結するプロパティ」を選ぶ必要がある。
これを業界では「コンポジットオンリー・プロパティ(Compositor-only properties)」と呼ぶ。
黄金の2大プロパティ
コンポジタースレッドだけで処理できるのは、基本的に以下の2つだけだ。
- `transform` (translate, scale, rotate など)
- `opacity`
これら以外のプロパティ(例えば `width`, `height`, `top`, `left`, `margin`, `background-color` など)をアニメーションさせたりスクロール連動させたりすると、ブラウザは「レイアウトの再計算(Layout)」や「再描画(Paint)」を毎フレームやり直す必要が生じるため、容赦なくメインスレッドが呼び出され、盛大にカクつく。
現場で後輩によく見せる、最もパフォーマンスが最適化されたアニメーションとスクロール制御のコード例を置いておく。そのままコピーしてプロジェクトのベースにしてくれて構わない。
スクロールしてパフォーマンスを確認せよ
このボックスは、JavaScriptのスクロールイベントを直接重く処理せず、CSSとコンポジタースレッドの連携で滑らかに追従します。
—
4. チーフアーキテクトからの実践的なアドバイス
実務でコードレビューをしていると、次のような事故を本当によく見かける。
1. `will-change: all` や何でもかんでもへの `will-change` の付与
- 「とりあえず速くなりそうだから」と全ての要素に `will-change` を書く奴がいるが、これは最悪だ。ブラウザは各レイヤーを保持するために莫大なGPUメモリ(VRAM)を消費するため、メモリ不足(OOM)を引き起こしてタブごとクラッシュするか、逆に描画パフォーマンスが低下する。レイヤーへの昇格は必要最小限に抑えろ。
2. `passive: true` の付け忘れ
- スマホのタッチ・スクロールやマウスホイールのイベントリスナーで `{ passive: true }` を忘れているコードを見つけたら、即座に修正させろ。これがないと、ブラウザは「JavaScript側でスクロールをキャンセル(`preventDefault()`)するかもしれない」と疑ってしまい、コンポジタースレッドが勝手にスクロールを進めることができず、スクロールの初動に顕著な「引っかかり(Jank)」が生じる。
—
まとめ
Webブラウザのレンダリングパイプライン、特にコンポジタースレッドの挙動を理解することは、単なる「知識のインプット」ではなく、「ユーザー体験というビジネス価値を泥臭く守り抜くための武器」だ。
画面がカクつく原因が、JavaScriptの処理遅延なのか、それともメインスレッドを強制的に巻き込んでいる不適切なCSSプロパティのせいなのか。Chrome DevToolsの「Performance」タブを開き、MainとCompositorのタイムラインを睨みつければ、ブラウザが裏側で何に苦悩しているかが手に取るようにわかるはずだ。
次にUIの実装で迷ったら、こう自問してほしい。
「このアニメーション、メインスレッドに頼らず、コンポジタースレッドだけで完結させられているか?」
その視点を持てた瞬間から、君の書くフロントエンドコードは見違えるほど洗練され、圧倒的に滑らかなプロダクトへと生まれ変わるはずだ。さあ、エディタに戻ってコードを最適化しに行こう。

コメント