ブラウザのピクセルパイプライン図解
なぜ transform は速く width は遅いのか、その境界が一段ずつ追える。DOM+CSSOM からレイアウト・ペイント・レイヤー・合成まで、各段の入出力と再実行範囲を文章で図解。
- 1.ピクセルパイプラインは Style→Layout→Paint→Layer化→Raster→Composite の一方向。前段の出力が後段の入力で、途中の段を飛ばせるかどうかが速さを決める。
- 2.境界は3つ。位置・サイズが変わればレイアウトから(リフロー)、見た目だけならペイントから(リペイント)、transform/opacity ならコンポジットだけで済む。
- 3.レイヤーは無料ではない。合成だけで動かすには事前に別レイヤー化が要り、メモリと初期コストを払う。will-change の貼りすぎは逆効果。
パイプラインの全体像と不変条件
ブラウザが DOM の変更を画面のピクセルに変えるまでは、明確な順序を持つ一方向のパイプラインです。各段は前段の出力を入力に取り、自分の出力を次段へ渡します。重要なのは、この順序は飛ばせても逆流しないこと——座標が未確定の要素は塗れないので、ペイントがレイアウトより前に来ることはありません。
[DOM] + [CSSOM]
│ ① Style 入力: DOM, CSSOM → 出力: 各要素の算出値つきレンダーツリー
▼
[Render Tree]
│ ② Layout 入力: レンダーツリー → 出力: 各ボックスの幾何(x,y,w,h)
▼
[Box Geometry]
│ ③ Paint 入力: 幾何+スタイル → 出力: 描画命令リスト(display list)
▼
[Display List]
│ ④ Layerize 入力: 描画命令+昇格条件 → 出力: 合成レイヤーの木
▼
[Layer Tree]
│ ⑤ Raster 入力: レイヤーごとの描画命令 → 出力: ビットマップ(タイル)
▼
[Bitmaps]
│ ⑥ Composite 入力: レイヤー+変換行列 → 出力: 画面1枚
▼
[Screen]
各段の「入力が変わらなければ再実行しない」という性質が、後述する3つの境界の正体です。
① Style:レンダーツリーへの合流
入力は DOM と CSSOM、出力は算出値(computed value)を全要素に解決したレンダーツリーです。ここでブラウザは、各 DOM 要素にマッチするセレクタを集め、詳細度とカスケード順で勝者を決め、継承と相対値の一部を解決します。
レンダーツリーは DOM と一対一ではありません。display: none の要素は載らず、::before のような擬似要素は DOM に無くても載ります。<head> や <script> のように描画されないノードも除外されます。Style 段の出力が変わるのは、クラス付け替えやインラインスタイル変更など、マッチ結果や算出値に影響する変更だけです。
② Layout:幾何の確定(リフローの段)
入力は算出値つきレンダーツリー、出力は各ボックスの座標とサイズ(レイアウトツリー / ボックスツリー)です。width: 50% のような相対値を、包含ブロック(containing block)を基準に実ピクセルへ落とします。フロー、Flexbox、Grid などフォーマッティングコンテキストごとにアルゴリズムが分岐します。
1要素のサイズ変更は、兄弟の押し出しや親の高さ再計算を通じて連鎖します。ブラウザは変更箇所に dirty フラグを立て、影響範囲を上下に伝播させて必要な部分木だけ再計算しますが、ルート近くの要素やテーブルレイアウトでは「必要な部分木」がほぼ全体になることがあります。これが「レイアウトは重い」と言われる実体です。
JS からの強制同期レイアウトにも注意が要ります。スタイルを書き換えた直後に offsetTop や getBoundingClientRect() など幾何値を読むと、ブラウザは正しい値を返すためその場でレイアウトを走らせます。書き→読みをループで交互に行うと毎回これが起き、いわゆるレイアウトスラッシングになります。対策は読みを先にまとめ、書きを後にまとめることです。
// NG: 書いた直後に読む → 毎回その場でレイアウトを強制
for (const el of items) {
el.style.width = el.offsetWidth + 10 + 'px';
}
// OK: 読みをまとめてから書きをまとめる(同期レイアウトは1回で済む)
const ws = items.map(el => el.offsetWidth);
items.forEach((el, i) => { el.style.width = ws[i] + 10 + 'px'; });
③ Paint:描画命令リストの生成
入力は確定した幾何とスタイル、出力は描画命令のリスト(ディスプレイリスト)です。注意したいのは、Paint 段はまだピクセルを作らないこと。ここで生成されるのは「ここに角丸の矩形を塗れ」「この影をこの色で描け」という命令の列であり、実際にビットマップへ変換するのは後段の Raster です。文字・背景・枠線・影など、幾何を変えない見た目の変更はこの Paint 段からやり直されます(リペイント)。
ペイントは要素ごとに**ペイント順(stacking order)**を持ちます。z-index や position、opacity などが重なり順(stacking context)を決め、これが次のレイヤー化の下地になります。
④ Layerize:合成レイヤーへの昇格
入力は描画命令と昇格条件、出力は合成レイヤーの木です。ページ全体は1枚で塗るとは限らず、特定の要素は独立したレイヤーに切り出されます。レイヤーに分かれていると、そのレイヤーだけを動かす・透過させる操作を、他を塗り直さずに合成段だけで実現できます。
どの要素が別レイヤーへ昇格するかには条件があります。代表的なものは次のとおりです。
| 昇格のきっかけ | 例 | ねらい |
|---|---|---|
| 3D変換や will-change | transform: translateZ(0) / will-change: transform | 合成だけで動かす準備を先にしておく |
| アニメーション中のプロパティ | transform/opacity をアニメーションする要素 | 毎フレームのリペイントを避ける |
| 特定の合成プロパティ | position: fixed, 一部の filter, video/canvas | スクロールや再生で他と独立して動く |
別レイヤー化すると、そのレイヤー用のビットマップをメモリに確保し、初回ラスタライズのコストも払います。will-change: transform を多数の要素へ恒久的に貼ると、レイヤー爆発でメモリを食いつぶし、かえって遅くなります。will-change は「直前に予約し、終わったら外す」一時的な合図として使うのが正解です。
⑤ Raster と ⑥ Composite:ピクセルの生成と合成
**Raster(ラスタライズ)**は、各レイヤーの描画命令を実際のビットマップ(多くはタイル単位)へ変換する段です。ここで初めてピクセルが生まれます。**Composite(コンポジット)**は、ラスタライズ済みのレイヤー群を、各レイヤーが持つ変換行列(transform)や不透明度(opacity)を適用しながら正しい重なり順で重ね、最終画面を1枚にまとめます。
この最後の2段はコンポジタスレッドと GPU が担うのが要点です。transform と opacity の変化は、レイヤーの中身(ビットマップ)を作り直さず、合成時のパラメータを差し替えるだけで済みます。だからメインスレッドの Layout/Paint をまるごと飛ばせ、スクロール中でも 60fps を保ちやすいのです。
3つの境界:どこからやり直すか
ここがパイプライン図解の核心です。何を変えたかで、再実行が始まる段が決まります。
| 変更の例 | 再実行が始まる段 | 走る工程 | 相対コスト |
|---|---|---|---|
| width / height / 要素の追加削除 / フォント / display | ② Layout | Layout→Paint→Raster→Composite | 高 |
| color / background / box-shadow / visibility | ③ Paint | Paint→Raster→Composite | 中 |
| transform / opacity (昇格済みレイヤー) | ⑥ Composite | Composite のみ | 低 |
つまり高速化の原理は「開始段を後ろへずらす」ことに尽きます。left/top で動かすとレイアウトから、background-position で動かすとペイントから、transform: translate() で動かせば合成だけ——同じ「動く」でもパイプラインに乗る量がまるで違います。
transform/opacity が「合成だけ」で済むのは、その要素がすでに別レイヤーへ昇格しているときです。昇格していなければ、初回にレイヤー化のためのペイントが一度走ります。アニメーション直前に will-change を立てておくと、この初回コストをフレーム外へ追い出せます。なお opacity でも、子要素の重なりやブレンドの都合でリペイントを誘発する場合がある点には注意してください。
contain: layout paint は、子のレイアウト・ペイントの影響をその要素の内側に閉じ込め、外への連鎖(リフローの伝播)を断ち切ります。content-visibility: auto は画面外の要素のレイアウトとペイントそのものをスキップし、初期表示の Layout/Paint 量を削ります。どちらも「パイプラインに乗る要素数」を構造的に減らす上級の打ち手です。
まとめ
ピクセルパイプラインは Style→Layout→Paint→Layerize→Raster→Composite の一方向で、各段は前段の出力を入力に取ります。再実行が始まる段は変更内容で決まり、境界は3つ——位置・サイズなら Layout から(リフロー)、見た目だけなら Paint から(リペイント)、transform/opacity なら Composite だけ。最後の2段はコンポジタスレッドと GPU が担うため、合成専用プロパティで動かすと速い。ただしそのためには事前のレイヤー昇格が要り、メモリと初回コストを払う点を忘れずに。基礎は ブラウザのレンダリング、入力側は DOM と CSS、計測の指標は Web パフォーマンス を合わせてどうぞ。
Web/フロントエンド Article
ブラウザのピクセルパイプライン図解を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
ブラウザ
比較で見る軸
難易度: advanced / カテゴリ: Web/フロントエンド / タグ数: 5
導入後に効く点
境界は3つ。位置・サイズが変わればレイアウトから(リフロー)、見た目だけならペイントから(リペイント)、transform/opacity ならコンポジットだけで済む。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- Web/フロントエンド
- タグ数
- 5
判断チェックリスト
- 自社の用途が「ブラウザ / レンダリング」に近いか確認する。
- 強みである「ピクセルパイプラインは Style→Layout→Paint→Layer化→Raster→Composite の一方向。前段の出力が後段の入力で、途中の段を飛ばせるかどうかが速さを決める。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。