レンダリングパイプライン詳説(スタイル→レイアウト→ペイント→コンポジット)
なぜ transform は速く width は遅いのかを内部から説明できるようになる。スタイル計算からGPU合成までの各段と、リフロー回避の原理を詳説します。
- 1.ピクセルパイプラインはスタイル(計算値解決)→レイアウト(ジオメトリ確定)→ペイント(描画命令の記録)→ラスタライズ→コンポジット(GPUでのタイル合成)の順で、上流が変わると下流が連鎖的に無効化される。
- 2.transform と opacity はコンポジタスレッドだけで処理でき、レイアウト・ペイントを発生させない。だから 60fps を保ちやすく、メインスレッドが詰まっても動き続ける。
- 3.will-change と transform:translateZ(0) は要素を専用の合成レイヤーへ昇格させ、再ペイント範囲を切り離す指示。ただし乱用するとレイヤー爆発でメモリと合成コストが増える。
パイプライン全体像と「無効化の伝播」
ブラウザがDOMとCSSOMを画面のピクセルへ変換する工程は、一般にピクセルパイプラインと呼ばれます。Chromium(RenderingNG)では概ね次の段に分かれます。
- スタイル(Style):各要素の**計算済みスタイル(computed style)**を解決する。
- レイアウト(Layout):各ボックスの位置とサイズを確定し、レイアウトツリー(フラグメントツリー)を作る。
- プレペイント(Pre-paint):プロパティツリー(transform / clip / effect / scroll)を構築する。
- ペイント(Paint):実際の塗りではなく、**描画命令のリスト(display list)**を記録する。
- ラスタライズ(Raster):display list をタイル単位で実ピクセルに変換する。
- コンポジット(Composite):レイヤー/タイルをGPUで合成し、画面へ出す。
最重要の原理は無効化(invalidation)の伝播です。ある段の入力が変わると、その段以降がすべてやり直しになります。スタイルが変われば全段、レイアウトが変わればレイアウト以降、ペイントだけ変わればペイント以降。逆にコンポジットの入力だけを変えれば、上流の重い段を丸ごとスキップできます。これが「transform は速い」の根拠です。基礎の流れは ブラウザのレンダリング も参照してください。
スタイル:計算値の解決とリキャルク範囲
スタイル段の実体はマッチングとカスケード解決です。各要素について、適用されうる宣言を詳細度・由来・順序で並べ、継承を解決し、相対値(em、%、currentColor など)を計算値へ畳み込みます。
specified value → computed value(継承・相対値を解決)→ used value(レイアウトで確定)
ここで効くのがスタイル無効化の局所化です。ブラウザはセレクタの依存関係を追跡し、変更があった要素のサブツリーだけを再計算しようとします。.parent.active .child のような子孫セレクタは、祖先のクラス変化で広い範囲の再マッチを誘発します。一方 CSS カスタムプロパティ(変数)は、その変数を参照する要素のスタイルを無効化するため、:root 上の1変数を毎フレーム書き換えると広範なリキャルクを招きます。詳細度とカスケードの基礎は CSS を参照してください。
レイアウト:ジオメトリ確定とリフローの連鎖
レイアウト段は計算済みスタイルとボックスツリーから、各フラグメントの**幾何(座標・寸法・行ボックス)**を解きます。width:50% のような used value もここで実ピクセルへ確定します。
コストの本質は双方向の依存です。ブロックの高さは子の高さ合計に依存し、フレックス/グリッドの子サイズは親の利用可能幅に依存します。そのため1要素の寸法変更が祖先・兄弟・子孫へ波及し、**リフロー(再レイアウト)**が連鎖します。これを抑える仕組みが contain です。
| 指定 | 意味 | 効果 |
|---|---|---|
| contain: layout | 内部レイアウトを外と分離 | 子の変化が外へ波及しない |
| contain: paint | 描画を境界でクリップ | 外へはみ出さない=再ペイント範囲を限定 |
| content-visibility: auto | 画面外を計算スキップ | 初期レイアウト/描画を遅延 |
JS で「スタイルを書く → 直後に offsetTop 等のジオメトリを読む」を交互に繰り返すと、ブラウザは読み取りのたびにその場で同期的にレイアウトを完了させられます。1フレーム内で何度もレイアウトが走り、急激にカクつきます。対策は読みをまとめてから書きをまとめること、または requestAnimationFrame でフレーム境界に揃えることです。
// レイアウトを強制トリガするプロパティ読み取りの例
el.offsetWidth; el.offsetTop; el.getBoundingClientRect();
el.scrollHeight; getComputedStyle(el).height;
// これらを「書き込みの直後」に読むと強制同期レイアウトになる
ペイントとプロパティツリー:塗らずに「記録」する
ペイント段は誤解されがちですが、この時点ではピクセルを塗りません。各レイヤーに対して「ここに矩形、ここにテキスト、ここに影」という**描画命令のリスト(display list)**を記録するだけです。重なり順(stacking context、z-index)の解決もここに関わります。
プレペイントで構築されるプロパティツリーが鍵です。transform・clip・opacity(effect)・scroll をツリー状の独立データとして持つことで、要素の transform 値が変わっても display list を再記録せず、プロパティツリーのノード値だけ差し替えてコンポジットへ渡せます。これが後述の高速パスの土台です。
ラスタライズとコンポジット:レイヤー・タイル・GPU
display list はラスタライズで実ピクセルへ変換されます。Chromium ではビューポートをタイル(例 256x256 等)に分割し、可視領域に近いタイルを優先してラスタライズします(多くは別スレッド/GPUラスタライズ)。
合成の単位が**合成レイヤー(compositing layer)**です。ある要素が独立レイヤーへ昇格すると、その内容は専用のテクスチャとして保持され、位置や不透明度の変更はテクスチャの再描画なしに実現できます。レイヤー昇格のきっかけ(コンポジットトリガ)には次のようなものがあります。
| トリガ | レイヤー昇格 | 備考 |
|---|---|---|
| will-change: transform / opacity | する | 事前昇格を明示。最も推奨される指定 |
| transform: translateZ(0) / translate3d | する | いわゆるハック。GPU昇格を強制する旧来手法 |
| position: fixed / sticky | しやすい | スクロールと独立に動かすため |
| video / canvas / iframe | する | 別途合成される要素 |
最終段のコンポジットは、専用のコンポジタスレッドがプロパティツリー(特に transform / scroll / effect)を読み、各レイヤーのタイルをGPUで重ね合わせて画面へ出します。重要なのは、この処理がメインスレッドから独立している点です。
なぜ transform / opacity / will-change がリフローを回避できるのか
ここまでを踏まえると、原理は一本の線でつながります。
transformとopacityはプロパティツリーのノード値として表現される。値の変更はそのノードを書き換えるだけで、スタイル・レイアウト・ペイント(display list 再記録)・ラスタライズのいずれも発生しない。コンポジタスレッドが新しい行列/不透明度でタイルを再合成するだけで済む。- 一方
left/top/width/marginはレイアウトの入力そのもの。変更はレイアウトから全段の無効化を起こし、リフロー→リペイント→再ラスタライズ→合成と全工程が走る。 will-change: transformは「この要素はこれから transform で動く」とブラウザに予告し、あらかじめ専用レイヤーへ昇格させる。昇格済みなら、アニメーション開始時にレイヤー生成と一度の再ペイントが発生せず、初手からコンポジットのみの高速パスに乗る。
transform / opacity のアニメーション(CSS アニメーションや Web Animations API)はコンポジタスレッドで駆動できます。つまりメインスレッドが重い JS で詰まっていても、スクロールやトランジションは 60fps を保てます。left で動かすと毎フレーム メインスレッドのレイアウトが必要になり、JS が詰まれば即カクつきます。
/* 良い:合成のみ。コンポジタスレッドで完結し、リフローもリペイントも起きない */
.card { will-change: transform; }
.card:hover { transform: translateY(-8px) scale(1.02); }
/* 悪い:top はレイアウトの入力。毎フレーム リフロー→リペイント→合成の全工程 */
.card-bad:hover { top: -8px; }
will-change や translateZ(0) を多数の要素に常時付けると、それぞれが専用レイヤー(テクスチャ)を確保し、GPUメモリの消費と合成パスのコストが増大します(レイヤー爆発)。アニメーション直前に付与し、終了後に外すのが理想です。will-change は「常用するスタイル」ではなく「短期的な最適化ヒント」だと捉えてください。
実務での適用指針
パフォーマンス系の問いでは、(1) パイプラインの段順と無効化の伝播、(2) transform / opacity が合成のみで完結する理由(プロパティツリー+コンポジタスレッド)、(3) 強制同期レイアウトの発生条件(書いた直後にジオメトリを読む)、(4) will-change がレイヤー昇格のヒントである点とその副作用、の4点が頻出です。
- 動かすなら transform / opacity:アニメーションは可能な限りこの2つに寄せる。
width/top/box-shadowのアニメーションは全工程を誘発する。 - 読み書きを分離:DOM のジオメトリ読み取りと書き込みをフレーム内でまとめ、強制同期レイアウトを避ける。
- 無効化を封じ込める:独立して更新される領域に
containやcontent-visibilityを当て、再レイアウト・再ペイントの範囲を物理的に切る。 - ヒントは局所的に:
will-changeはアニメーションする要素に限定し、終わったら外す。
まとめ
ピクセルパイプラインはスタイル→レイアウト→プレペイント→ペイント(display list 記録)→ラスタライズ→コンポジットの順で、上流の変更が下流を連鎖的に無効化します。transform と opacity はプロパティツリーのノード値として表現され、コンポジタスレッドが再合成するだけで完結するため、レイアウトもペイントも起こさず、メインスレッドが詰まっても 60fps を保てます。will-change はその高速パスへ乗せるためのレイヤー昇格ヒントですが、乱用はレイヤー爆発を招きます。仕上げに Web パフォーマンス と DOM 操作の観点を合わせると、計測から改善までの筋道が通ります。
Web/フロントエンド Article
レンダリングパイプライン詳説(スタイル→レイアウト→ペイント→コンポジット)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
レンダリング
比較で見る軸
難易度: advanced / カテゴリ: Web/フロントエンド / タグ数: 5
導入後に効く点
transform と opacity はコンポジタスレッドだけで処理でき、レイアウト・ペイントを発生させない。だから 60fps を保ちやすく、メインスレッドが詰まっても動き続ける。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- Web/フロントエンド
- タグ数
- 5
判断チェックリスト
- 自社の用途が「レンダリング / ブラウザ」に近いか確認する。
- 強みである「ピクセルパイプラインはスタイル(計算値解決)→レイアウト(ジオメトリ確定)→ペイント(描画命令の記録)→ラスタライズ→コンポジット(GPUでのタイル合成)の順で、上流が変わると下流が連鎖的に無効化される。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。