こんにちは。フロントエンドの現場で日々、パフォーマンスチューニングや不可解な描画バグと格闘している君なら、一度は「なぜこのCSSを書いただけでブラウザが重くなるんだ?」と頭を抱えたことがあるはずだ。
「CSSを書いたら画面が変わる」——それは初心者向けの魔法の言葉だ。しかし、我々プロのフロントエンドエンジニアにとって、その裏側でブラウザ(BlinkやWebKitなどのレンダリングエンジン)がどれほど過酷な計算処理をミリ秒単位で行っているかを知ることは、プロダクトのUXを保つ上で必須の教養だ。
今回は、DOMが構築された後の主役である「Recalculate Style(スタイル計算)」「Layout(レイアウト)」「Paint(ペイント)」「Composite(合成)」というレンダリングパイプラインの4つのステージについて、ブラウザの内部挙動を丸裸にしていこう。
—
1. Recalculate Style(スタイル計算):CSSの膨大なセレクタと向き合う魔境
HTMLがパースされてDOMツリーができあがり、同時に外部CSSやインラインスタイルがCSSOM(CSS Object Model)へと変換されると、ブラウザは即座にRecalculate Styleのステージに入る。
ここでブラウザがやっていることは単純ではない。すべてのDOMノードに対して、「どのCSSセレクタがマッチするのか」を総当たり、あるいは効率的なハッシュアルゴリズムを用いて判定し、各要素に適用される最終的なスタイル(Computed Style)を決定するのだ。
現場の知見:セレクタの右側(Key Selector)の重要性
CSSを書くとき、私たちはついつい詳細度(Specificity)の高いセレクタを書きがちだ。しかし、ブラウザはスタイルを適用する際、セレクタの「右側(一番末尾)」から左側に向かってマッチングを行う(これをKey Selectorと呼ぶ)。
`div.container ul li a` というセレクタがあれば、ブラウザはまずドキュメント内のすべての `` タグを探し、その親が `
—
2. Layout(レイアウト / リフロー):すべての座標を決定する重労働
スタイルが確定したら、次はLayout(Gecko系ではReflowと呼ばれる)のステージだ。ここでは、「画面上のどこに、どれくらいのサイズでその要素を配置するのか」という幾何学的な計算(Geometry)が行われる。
親要素のサイズが変われば、子要素のサイズも変わり、さらにそれが兄弟要素や親の親にまで連鎖する。これが「リフロー」と呼ばれる現象の正体だ。ブラウザはツリー構造を再帰的にトラバースし、すべての要素の正確なピクセル座標(x, y)とボックスの大きさを算出する。
実務での罠:強制同期レイアウト(Layout Thrashing)
JavaScriptでDOMのスタイルを「読み取ってから書き換える」をループ内で交互に行うと、最悪のパフォーマンスを引き起こす。これがLayout Thrashing(レイアウト地獄)だ。
以下の「やってはいけない」アンチパターンを見てほしい。
// 【アンチパターン】Layout Thrashingを引き起こす最悪のコード例
const boxes = document.querySelectorAll(‘.box’);
boxes.forEach(box => {
// 1. offsetWidthを読み取るために、ブラウザは「その瞬間のLayoutを強制」される
const currentWidth = box.offsetWidth;
// 2. スタイルを書き換える
box.style.width = `${currentWidth + 10}px`;
// ループの次の周回でまた 1に戻る…
// これが数dozen(ダース)続くと、フレームレートは一気に墜落する。
});
このコードでは、ループの回数分だけレイアウトの計算が強制同期(Synchronous Layout)され、メインスレッドが完全にフリーズする。
対策は簡単で、「読み込み」と「書き込み」のフェーズを綺麗に分離(Batching)することだ。一気に読み込んで、一気に書き換える。これだけでブラウザの負担は激減する。
—
3. Paint(ペイント):ピクセルを塗る前の「お絵描き指示書」作成
Layoutで「どこに何を描くか(座標)」が決まったら、次はPaintのステージだ。
勘違いしやすいが、この段階で実際に画面のピクセルが塗られているわけではない。Paintとは、文字を描く、背景色を塗る、ボーダーやシャドウを描画するといった描画の「手順(コマンドのリスト)」を記録する作業である。
ブラウザはこのステージで、要素をレイヤー単位で分解し、それぞれの描画順序を記録した「Paint Records」を作成する。CSSの `box-shadow` や複雑なグラデーション、テキストの描画が多いほど、このPaint Recordsの生成コストは跳ね上がる。
—
4. Composite(合成):GPUの力を借りて画面を構築する最終防衛ライン
最後に登場するのがCompositeステージだ。Paintで生成された個々のレイヤー(お絵描きの指示書)を、最終的に一つの画像として重ね合わせ、画面に出力(Rasterize)する。
ここで主役になるのがGPU(グラフィックスボード)だ。
最新のブラウザは、特定の条件を満たしたレイヤーをGPUに転送し、合成処理をハードウェアアクセラレーションで行う。これにより、メインスレッド(CPU)を解放し、滑らかなアニメーションを実現している。
現場ですぐ使えるTips:GPUレイヤーを昇格させる魔法
CSSプロパティの一部には、ブラウザに「この要素を独立したレイヤーとしてGPUに送ってくれ」と強制的に指示できるものがある。代表格が `will-change` だ。
/ 【ベストプラクティス】アニメーションさせる要素には事前にヒントを与える /
.modal-dialog {
will-change: transform, opacity;
/ または古い手法として trnasform: translateZ(0); が使われることもあるが、今は will-change が標準的 /
}
アニメーションがカクつく原因の多くは、毎フレームごとにLayoutやPaintが走っていることにある。
もし要素を移動させたり拡大縮小させたりするなら、`top`や`left`(これらはLayoutを引き起こす)を使うのではなく、`transform: translateX()` や `opacity` を使おう。これら二つのプロパティは、LayoutとPaintのステージを完全にバイパスし、Compositeステージ(GPU合成)だけで処理が完結するため、圧倒的に滑らか(60fps / 120fps)に動くのだ。
—
まとめ:フロントエンドエンジニアが意識すべき「レンダリングのコスト」
ブラウザのレンダリングパイプラインを要約すると、こうなる。
1. Recalculate Style: CSSのセレクタを解釈し、適用スタイルを決める
2. Layout: 要素のサイズと位置を計算する(★CPU負荷が高い)
3. Paint: 描画の指示書を作る(★描画コストが高い)
4. Composite: レイヤーを重ね合わせて画面に出力する(★GPUで高速化可能)
フロントエンドの実装において、私たちが書くJavaScriptやCSSが、このパイプラインのどこを刺激しているかを常に頭の中でシミュレーションできるようになれば、もう「なぜか重いWebサイト」に悩まされることはなくなるはずだ。
パフォーマンスの最適化は、地味な作業の積み重ねだが、その裏側にあるブラウザのメカニズムを知っていれば、これほどエキサイティングで論理的な分野もない。
さあ、明日からのコードレビューでは、レイアウトスリッシングや無駄なスタイル計算を引き起こしていないか、シニアの視点で鋭くチェックしてやろうぜ。

コメント