ブラウザのレンダリングプロセス完全解説|HTMLからピクセルまでの舞台裏【図解】
ブラウザがHTML/CSS/JSをどう画面に表示するかを図解付きで完全解説。レンダリングパイプライン、Critical Rendering Path、リフロー・リペイントの最適化を学べる。
Web パフォーマンス最適化の第一歩は、ブラウザがどのように HTML を画面に描画しているかを理解することです。レンダリングパイプラインを知れば、なぜ特定の CSS プロパティが遅いのか、なぜ transform がアニメーションに適しているのかが論理的にわかるようになります。
レンダリングパイプラインの全体像
ブラウザは以下の 5 段階で HTML を画面に表示します。
HTML + CSS + JS
↓
1. Parse(パース)
↓
2. Style(スタイル計算)
↓
3. Layout(レイアウト)
↓
4. Paint(ペイント)
↓
5. Composite(コンポジット)
↓
画面表示
各ステップを詳しく見ていきましょう。
ステップ1: Parse(パース)
HTML パーサーは、HTML を上から順に読み取り、DOM ツリーを構築します。
<html>
<body>
<h1>Hello</h1>
<p>World</p>
</body>
</html>
Document
└ html
└ body
├ h1
│ └ "Hello"
└ p
└ "World"
同時に CSS パーサーが CSS を解析し、CSSOM ツリーを構築します。
JavaScript によるブロッキング
重要なのは、JavaScript は HTML パースをブロックすることです。
<p>ここまでは描画される</p>
<script src="heavy.js"></script>
<p>この段落は script 完了後まで描画されない</p>
対策: async または defer を使う。
<!-- パースと並行してダウンロード、ダウンロード完了時に実行 -->
<script src="app.js" async></script>
<!-- パースと並行してダウンロード、パース完了後に実行 -->
<script src="app.js" defer></script>
ステップ2: Style(スタイル計算)
DOM ツリーと CSSOM ツリーを結合し、各要素に適用されるスタイルを決定します。これは Render Tree と呼ばれます。
DOM + CSSOM → Render Tree
Render Tree には display: none の要素は含まれません(visibility: hidden は含まれます)。
セレクタのパフォーマンス
CSS セレクタは右から左へ評価されるため、右端がシンプルな方が高速です。
/* 遅い: すべての p に対して祖先を探す */
div.container div.content p { color: red; }
/* 高速 */
.content-text { color: red; }
ただし、現代ブラウザではほとんどのセレクタが十分高速なため、神経質になりすぎる必要はありません。
ステップ3: Layout(レイアウト)
各要素の位置とサイズを計算します。リフロー(Reflow)とも呼ばれます。
- 要素の幅、高さ、位置
- テキストの折り返し
- 他の要素との関係
レイアウトは最も高コストなステップの一つです。特定のプロパティを変更すると再計算が走ります。
レイアウトを引き起こすプロパティ
width, height, padding, margin, border
top, left, right, bottom
font-size, font-family
display, position, float, overflow
これらを JavaScript で頻繁に変更するとパフォーマンスが悪化します。
ステップ4: Paint(ペイント)
各要素を実際のピクセルに変換します。
- 背景色、画像
- 文字
- 枠線
- シャドウ
ペイントだけ引き起こすプロパティ
color, background-color, background-image
border-color, outline-color
visibility, box-shadow, text-shadow
レイアウトは走らず、ペイントだけ発生するため、中程度のコストです。
ステップ5: Composite(コンポジット)
ペイントされた結果をレイヤーとして GPU に送り、合成して画面に表示します。
コンポジットだけで済むプロパティ
transform, opacity, filter
これらはメインスレッドを使わず GPU で処理されるため、最も高速です。60fps のアニメーションに最適です。
CSSトリガーマップ
| プロパティ変更 | Layout | Paint | Composite |
|---|---|---|---|
width | ✓ | ✓ | ✓ |
background-color | - | ✓ | ✓ |
transform | - | - | ✓ |
opacity | - | - | ✓ |
結論: アニメーションには transform と opacity を使うのがベストです。
悪い例: width でアニメーション
.bad {
transition: width 0.3s;
}
.bad:hover {
width: 200px; /* Layout → Paint → Composite */
}
良い例: transform でアニメーション
.good {
transition: transform 0.3s;
}
.good:hover {
transform: scaleX(2); /* Composite のみ */
}
Critical Rendering Path(CRP)
初回描画までの経路を CRP と呼びます。これを最短化するのがパフォーマンス最適化の鍵です。
HTML downloaded
↓
DOM built
↓
CSSOM built
↓
Render Tree built
↓
Layout
↓
Paint
↓
First Paint (FP)
↓
First Contentful Paint (FCP)
↓
Largest Contentful Paint (LCP) ← Core Web Vitals
レンダリングブロッキングリソース
CSS
CSS はデフォルトでレンダリングをブロックします。理由は、スタイル未確定の状態で描画すると FOUC(Flash of Unstyled Content)が発生するからです。
対策: Critical CSS をインライン化し、残りを遅延読み込み。
<head>
<style>
/* Above the fold のスタイルをインラインで */
.hero { ... }
</style>
<link
rel="preload"
href="/styles.css"
as="style"
onload="this.onload=null;this.rel='stylesheet'"
/>
</head>
JavaScript
<script> タグはデフォルトでパースをブロックします。前述の async / defer で解消できます。
リフローとリペイントの最適化
JavaScript でのバッチ処理
// 悪い例: 各行でリフローが発生
const el = document.getElementById('box');
el.style.width = '100px';
el.style.height = '100px';
el.style.padding = '10px';
// 良い例: 1回のリフローにまとめる
const el = document.getElementById('box');
el.style.cssText = 'width: 100px; height: 100px; padding: 10px;';
// または class で一括変更
el.classList.add('box-large');
レイアウトスラッシング(Layout Thrashing)
// 悪い例: 読み取り→書き込みの繰り返しで強制リフロー発生
for (let i = 0; i < items.length; i++) {
const width = items[i].offsetWidth; // 読み取り
items[i].style.width = (width * 2) + 'px'; // 書き込み
}
// 良い例: 読み取りと書き込みを分離
const widths = items.map(item => item.offsetWidth); // 読み取りバッチ
items.forEach((item, i) => {
item.style.width = (widths[i] * 2) + 'px'; // 書き込みバッチ
});
will-change ヒント
ブラウザに「このプロパティが変更される」と事前に伝えることで、新しいコンポジットレイヤーを準備させられます。
.animated {
will-change: transform;
}
注意: 乱用するとメモリ消費が増えます。アニメーション開始直前に付け、終了後に削除するのがベストプラクティスです。
element.addEventListener('mouseenter', () => {
element.style.willChange = 'transform';
});
element.addEventListener('mouseleave', () => {
element.style.willChange = 'auto';
});
GPU レイヤーの作成条件
以下のいずれかでブラウザは独立した GPU レイヤーを作ります。
transform: translateZ(0)(ハック)will-change: transformvideo,canvasopacityが 1 未満のアニメーションfilterプロパティposition: fixed
測定と検証
Chrome DevTools の Performance パネルで、レンダリングプロセスを詳細に確認できます。
- DevTools を開く
- Performance パネル
- 録画開始 → 操作 → 停止
- 紫 = Rendering(Layout)、緑 = Painting、黄 = Scripting
Rendering タブで以下も可視化できます。
- Paint flashing: ペイントが発生している箇所をハイライト
- Layout Shift Regions: レイアウトシフトを可視化
- Frame Rendering Stats: FPS とGPUメモリ表示
まとめ
ブラウザのレンダリングを理解すると、パフォーマンス改善の優先順位が明確になります。
重要なポイント:
- Layout > Paint > Composite の順にコストが高い
transformとopacityは最安- CSS と JS はレンダリングをブロックする → 最適化必須
- DOM 操作はバッチ処理
- DevTools で実測
理論を知った上で、Lighthouse や Performance パネルで実測すれば、効果的に改善できます。次のステップは、Core Web Vitals の改善です。