【テクニカル・上級編】 コンポジット(合成)の仕組み – Webブラウザの仕組み実践ガイド

ピクセルを物理限界で回せ:ブラウザ・コンポジット(合成)の極限アーキテクチャ

フロントエンドの世界において、「リフロー(Reflow / Layout)とリペイント(Repaint / Paint)を避けよ」という格言は、もはや耳にタコができるほど語り尽くされてきた。

しかし、我々が立ち向かう現代のWebアプリケーションは、数十万のDOMノード、絶え間なく動く高解像度のアニメーション、そしてモバイルから超高解像度ディスプレイ(Retina/8K)までカバーしなければならない苛烈な戦場だ。単に「`transform` を使えば速くなる」といった表面的な知識だけで、この戦場を生き抜くことはできない。

その先に待っているのは、メモリ(VRAM)の枯渇によるサイレントクラッシュ、スクロールが1フレームだけ引っかかる微細なガタつき(Jank)、そしてメインスレッドとCompositorスレッドの非同期な競合による描画のズレといった、泥臭く、かつ致命的なバグの数々である。

本稿では、ブラウザレンダリングの最終工程にして、GPUというハードウェアを直接ドライブする心臓部である「コンポジット(合成)」のアーキテクチャを解剖する。公式マニュアルのコピーではない。ピクセルが画面に物理的に描画されるその瞬間までに、ブラウザの内部で何が起きているのか、その深淵を覗きにいこう。

—

1. 救世主としての「コンポジタースレッド」

ブラウザのレンダリングを理解する上で、最も重要なパラダイムシフトは「メインスレッド(Main Thread)」と「コンポジタースレッド(Compositor Thread)」の完全な分離である。

JavaScriptの実行、DOMの構築、CSSの解析、レイアウト計算(リフロー)、そして実際のペイント(描画コマンドの生成)は、すべてシングルスレッドであるメインスレッドで行われる。ここは常に過密状態であり、JSの重い処理が走れば簡単にブロッキングが発生する。

もし、画面のスクロールやアニメーションの毎フレーム(1秒間に60回〜120回)をメインスレッドで処理していたら、Webはまともに動かない。そこで登場するのが、Chromium(Blink)で言えば `Chrome Compositor (cc)` と呼ばれる、別スレッドで動作するコンポジタースレッドだ。

[メインスレッド]
DOM / CSSJS ─> Layout ─> Paint (描画コマンドの生成)
│
(コミット / コピー)
▼
[コンポジタースレッド]
Tiling (タイル分割) ─> Raster (ビットマップ化) ─> Draw (GPUによる合成・画面出力)

コンポジットの役割は、画面をいくつかの「レイヤー(層)」に分割し、それぞれのレイヤーをあらかじめ画像(ビットマップ)として描画しておき、最終的な画面表示の際にGPUのメモリ(VRAM)上で重ね合わせて表示位置を調整するだけにすることだ。

この「重ね合わせるだけ」の処理こそが、GPUの得意とするテクスチャマッピング(テクスチャの描画)であり、CPUを介さずにミリ秒未満で実行できる。これが、`transform` や `opacity` を使ったアニメーションが、メインスレッドが100%ビジーで固まっていても滑らかに動き続ける理由である。

—

2. レイヤー昇格(Promote)のメカニズムと裏のコスト

では、すべての要素を個別のレイヤーにすれば、リペイントが一切発生しない最強のサイトができるのではないか?

答えは「ノー」だ。レイヤーの作成には、極めて重いコストが伴う。

2.1 RenderObject から GraphicsLayer へ

ブラウザ内部では、DOM tree から `RenderObject` tree が作られ、それが重なり順(Stacking Context)に基づいて `RenderLayer` tree へと整理される。そして特定の条件を満たした要素だけが、独自の描画メモリ領域を持つ `GraphicsLayer`(合成レイヤー)へと昇格(Promote)する。

昇格の主なトリガー(Composite Trigger)は以下の通りだ。

  • `transform` や `opacity` を用いた3D変形(例: `translate3d`, `translateZ`)
  • `