Webページがブラウザに表示されるまでには、HTMLの解析からピクセルの描画まで、複雑なプロセスが実行されています。このブラウザレンダリングの仕組みを理解することは、パフォーマンス最適化において非常に重要です。

この記事では、DOM構築からコンポジットまでのレンダリングパイプライン全体を詳細に解説し、クリティカルレンダリングパスの最適化やリフロー・リペイントを最小化する実践的なテクニックを紹介します。

この記事で学べること

  • ブラウザレンダリングの6つのステップ(DOM、CSSOM、レンダーツリー、レイアウト、ペイント、コンポジット)
  • クリティカルレンダリングパスとレンダーブロッキングリソースの回避方法
  • リフロー(レイアウト再計算)とリペイント(再描画)の違いと最小化テクニック
  • GPU合成を活用したスムーズなアニメーションの実装方法

前提知識と実行環境

この記事を理解するために必要な前提知識と実行環境は以下の通りです。

項目 内容
前提知識 HTML/CSS/JavaScriptの基礎知識
確認環境 Google Chrome 131以降、Chrome DevTools
対象読者 フロントエンドのパフォーマンス最適化に興味があるエンジニア、ブラウザの内部動作を理解したいWeb開発者

ブラウザレンダリングパイプラインの全体像

ブラウザがHTMLを受け取ってから画面に描画するまでの流れは、以下の6つのステップで構成されています。

flowchart LR
    subgraph パース
        A[HTML] --> B[DOM]
        C[CSS] --> D[CSSOM]
    end
    
    subgraph レンダリング
        B --> E[レンダーツリー]
        D --> E
        E --> F[レイアウト]
        F --> G[ペイント]
        G --> H[コンポジット]
    end
    
    H --> I[画面表示]

レンダリングパイプラインの各ステップ

ステップ 処理内容 出力
DOM構築 HTMLをパースしてDOMツリーを構築 DOMツリー
CSSOM構築 CSSをパースしてCSSOMツリーを構築 CSSOMツリー
レンダーツリー構築 DOMとCSSOMを結合して表示対象を決定 レンダーツリー
レイアウト 各要素の位置とサイズを計算 ボックスモデル
ペイント 各要素を実際のピクセルに変換 ペイントレコード
コンポジット レイヤーを合成して最終的な画面を生成 表示画面

DOM構築とCSSOM構築

DOM(Document Object Model)の構築

ブラウザはHTMLドキュメントを受信すると、バイトをトークンに変換し、トークンをノードに変換してDOMツリーを構築します。

flowchart TB
    A[バイト<br/>3C 68 74 6D 6C...] --> B[文字<br/>html head body...]
    B --> C[トークン<br/>StartTag: html<br/>StartTag: head...]
    C --> D[ノード<br/>HTMLElement<br/>HeadElement...]
    D --> E[DOMツリー]

以下のHTMLがどのようにDOMツリーに変換されるか見てみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>サンプルページ</title>
    <link rel="stylesheet" href="styles.css">
  </head>
  <body>
    <header>
      <h1>ブラウザレンダリング</h1>
    </header>
    <main>
      <p>DOMとCSSOMの解説</p>
    </main>
  </body>
</html>

このHTMLは以下のようなDOMツリーに変換されます。

flowchart TB
    document[document]
    html[html]
    head[head]
    body[body]
    meta[meta]
    title[title]
    link[link]
    header[header]
    main[main]
    h1[h1]
    p[p]
    
    document --> html
    html --> head
    html --> body
    head --> meta
    head --> title
    head --> link
    body --> header
    body --> main
    header --> h1
    main --> p

CSSOM(CSS Object Model)の構築

CSSOMはCSSをパースして構築されるオブジェクトモデルです。DOMと同様にツリー構造を持ち、CSSの継承とカスケードのルールが適用されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* styles.css */
body {
  font-family: 'Helvetica Neue', sans-serif;
  font-size: 16px;
  line-height: 1.6;
}

header {
  background-color: #333;
  color: #fff;
  padding: 20px;
}

h1 {
  font-size: 2rem;
  margin: 0;
}

main {
  padding: 20px;
}

p {
  color: #666;
}

CSSOMツリーでは、親要素のスタイルが子要素に継承されます。例えば、bodyに設定されたfont-familyは、すべての子孫要素に継承されます。

flowchart TB
    body["body<br/>font-family: Helvetica Neue<br/>font-size: 16px<br/>line-height: 1.6"]
    header["header<br/>background: #333<br/>color: #fff<br/>padding: 20px<br/>+ 継承スタイル"]
    main["main<br/>padding: 20px<br/>+ 継承スタイル"]
    h1["h1<br/>font-size: 2rem<br/>margin: 0<br/>+ 継承スタイル"]
    p["p<br/>color: #666<br/>+ 継承スタイル"]
    
    body --> header
    body --> main
    header --> h1
    main --> p

レンダーブロッキングリソースの問題

CSSはレンダーブロッキングリソースです。CSSOMが完成するまで、ブラウザはレンダーツリーを構築できません。これは、CSSOMなしでは要素のスタイルを決定できないためです。

sequenceDiagram
    participant Browser as ブラウザ
    participant Server as サーバー
    
    Browser->>Server: HTMLリクエスト
    Server-->>Browser: HTMLレスポンス
    Note over Browser: DOM構築開始
    Browser->>Server: CSSリクエスト
    Note over Browser: DOM構築完了
    Note over Browser: CSSOMを待機中...
    Server-->>Browser: CSSレスポンス
    Note over Browser: CSSOM構築
    Note over Browser: レンダーツリー構築開始

レンダーツリーの構築

レンダーツリーは、DOMツリーとCSSOMツリーを組み合わせて構築されます。このツリーには、画面に表示される要素のみが含まれます。

レンダーツリーに含まれない要素

以下の要素はレンダーツリーに含まれません。

  • <head><script><meta>などの非表示要素
  • display: noneが適用された要素
  • CSSで生成されないコンテンツ
1
2
3
<div class="visible">表示される要素</div>
<div class="hidden" style="display: none;">表示されない要素</div>
<div class="invisible" style="visibility: hidden;">非表示だがスペースを占有</div>

上記の例では、.hiddenはレンダーツリーに含まれませんが、.invisibleはレンダーツリーに含まれます。visibility: hiddenは要素を非表示にしますが、レイアウト上のスペースは確保されるためです。

display: none と visibility: hidden の違い

プロパティ レンダーツリー レイアウト リフロー発生
display: none 含まれない 占有しない 切り替え時に発生
visibility: hidden 含まれる 占有する 切り替え時に発生しない
opacity: 0 含まれる 占有する 切り替え時に発生しない

レイアウト(リフロー)

レイアウトステップでは、レンダーツリー内の各要素の正確な位置とサイズを計算します。この処理は「リフロー」とも呼ばれます。

レイアウト計算の流れ

  1. ビューポートのサイズを取得
  2. ルート要素から順にボックスモデルを計算
  3. 各要素の幅、高さ、マージン、パディング、ボーダーを決定
  4. 相対的な単位(%、em、rem)を絶対的なピクセル値に変換
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
.container {
  width: 80%;        /* ビューポート幅の80% */
  padding: 2rem;     /* ルート要素の2倍のフォントサイズ */
  margin: 0 auto;    /* 中央揃え */
}

.content {
  width: 50%;        /* 親要素(.container)の50% */
  font-size: 1.5em;  /* 親要素の1.5倍のフォントサイズ */
}

レイアウトを発生させるCSSプロパティ

以下のCSSプロパティを変更すると、レイアウト(リフロー)が発生します。

カテゴリ プロパティ
サイズ width, height, padding, margin, border-width
位置 top, left, right, bottom, position
フォント font-size, font-family, font-weight, line-height
テキスト text-align, vertical-align, white-space
その他 display, float, clear, overflow

ペイント

ペイントステップでは、レイアウトで計算された情報をもとに、各要素を実際のピクセルに変換します。

ペイントの順序(スタッキングコンテキスト)

ペイントは以下の順序で行われます。

  1. 背景色(background-color)
  2. 背景画像(background-image)
  3. ボーダー(border)
  4. 子要素
  5. アウトライン(outline)

ペイントのみを発生させるCSSプロパティ

以下のプロパティはレイアウトを発生させず、ペイント(リペイント)のみを発生させます。

カテゴリ プロパティ
color, background-color, border-color
box-shadow, text-shadow
その他 visibility, outline, background-image

コンポジット(合成)

コンポジットは、複数のレイヤーを合成して最終的な画面を生成するステップです。このステップはGPUで処理されるため、非常に高速です。

レイヤーが作成される条件

以下の条件を満たす要素は、独自の合成レイヤーを持ちます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/* 3D変換を使用 */
.layer-3d {
  transform: translateZ(0);
}

/* will-changeプロパティを使用 */
.layer-will-change {
  will-change: transform;
}

/* opacity が 1 未満 */
.layer-opacity {
  opacity: 0.99;
}

/* position: fixed を使用 */
.layer-fixed {
  position: fixed;
}

コンポジットのみを発生させるCSSプロパティ

以下のプロパティは、レイアウトやペイントを発生させず、コンポジットのみで処理されます。

プロパティ 説明
transform 要素の変形(移動、回転、拡大縮小)
opacity 透明度の変更

これらのプロパティは、GPUで処理されるため、60fpsのスムーズなアニメーションを実現できます。

クリティカルレンダリングパスの最適化

クリティカルレンダリングパスとは、ブラウザが最初のピクセルを描画するまでに必要な一連のステップです。このパスを最適化することで、First Contentful Paint(FCP)を改善できます。

1. レンダーブロッキングCSSの最適化

CSSはレンダーブロッキングリソースですが、適切な対策で影響を軽減できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!-- クリティカルCSSをインライン化 -->
<head>
  <style>
    /* ファーストビューに必要な最小限のCSS */
    body { margin: 0; font-family: sans-serif; }
    .header { background: #333; color: #fff; padding: 20px; }
    .hero { min-height: 100vh; display: flex; align-items: center; }
  </style>
  
  <!-- 非クリティカルCSSは非同期読み込み -->
  <link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
  <noscript><link rel="stylesheet" href="styles.css"></noscript>
</head>

2. メディアクエリによるCSSの分割

デバイスやビューポートに応じてCSSを分割することで、不要なCSSの読み込みを防ぎます。

1
2
3
4
5
6
7
8
<!-- 基本スタイル(常に読み込み) -->
<link rel="stylesheet" href="base.css">

<!-- 印刷用スタイル(印刷時のみブロッキング) -->
<link rel="stylesheet" href="print.css" media="print">

<!-- 大画面用スタイル(大画面時のみブロッキング) -->
<link rel="stylesheet" href="desktop.css" media="(min-width: 1024px)">

3. JavaScriptのパーサーブロッキング回避

JavaScriptはDOMのパースをブロックします。asyncdefer属性を使用して、パーサーブロッキングを回避しましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<!-- パーサーブロッキング(避けるべき) -->
<script src="blocking.js"></script>

<!-- 非同期読み込み(DOMパースをブロックしない、読み込み完了後すぐ実行) -->
<script async src="analytics.js"></script>

<!-- 遅延読み込み(DOMパース完了後に実行順序を保って実行) -->
<script defer src="app.js"></script>

<!-- モジュールスクリプト(デフォルトでdefer動作) -->
<script type="module" src="main.js"></script>

async と defer の違い

sequenceDiagram
    participant HTML as HTMLパース
    participant JS as JSダウンロード
    participant Exec as JS実行
    
    Note over HTML,Exec: 通常の script
    HTML->>HTML: パース中断
    JS->>JS: ダウンロード
    Exec->>Exec: 実行
    HTML->>HTML: パース再開
    
    Note over HTML,Exec: async 属性
    HTML->>HTML: パース継続
    JS->>JS: 並行ダウンロード
    HTML->>HTML: パース中断
    Exec->>Exec: ダウンロード完了後即実行
    HTML->>HTML: パース再開
    
    Note over HTML,Exec: defer 属性
    HTML->>HTML: パース継続
    JS->>JS: 並行ダウンロード
    HTML->>HTML: パース完了
    Exec->>Exec: パース完了後に実行
属性 ダウンロード 実行タイミング 実行順序 用途
なし ブロッキング 即座 記述順 DOM操作が必要なスクリプト
async 並行 ダウンロード完了後 不定 独立したスクリプト(アナリティクスなど)
defer 並行 DOMContentLoaded前 記述順 DOM操作が必要なスクリプト

リフローとリペイントの最小化

リフロー(レイアウト再計算)は、ブラウザレンダリングにおいて最もコストの高い処理です。パフォーマンス最適化では、リフローの発生回数を最小限に抑えることが重要です。

リフローを引き起こす操作

以下の操作はリフローを引き起こします。

1
2
3
4
5
6
7
8
9
// リフローを引き起こすDOM操作
element.offsetHeight;          // レイアウト情報の読み取り
element.getBoundingClientRect();
window.getComputedStyle(element);

// リフローを引き起こすスタイル変更
element.style.width = '100px';
element.style.height = '200px';
element.style.padding = '10px';

強制同期レイアウト(Layout Thrashing)を避ける

読み取りと書き込みを交互に行うと、ブラウザは毎回レイアウトを再計算する必要があります。これを「強制同期レイアウト」または「Layout Thrashing」と呼びます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 悪い例:読み取りと書き込みを交互に行う(強制同期レイアウト)
function updateElements(elements) {
  elements.forEach(element => {
    const height = element.offsetHeight; // 読み取り → リフロー発生
    element.style.height = height + 10 + 'px'; // 書き込み
  });
}

// 良い例:読み取りと書き込みをバッチ処理
function updateElementsOptimized(elements) {
  // まず全ての読み取りを行う
  const heights = elements.map(element => element.offsetHeight);
  
  // 次に全ての書き込みを行う
  elements.forEach((element, index) => {
    element.style.height = heights[index] + 10 + 'px';
  });
}

requestAnimationFrame を使用した最適化

requestAnimationFrameを使用することで、ブラウザの描画タイミングに合わせてDOM操作を行えます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 悪い例:同期的なDOM更新
function animateBad(element) {
  let position = 0;
  setInterval(() => {
    position += 1;
    element.style.left = position + 'px';
  }, 16);
}

// 良い例:requestAnimationFrameを使用
function animateGood(element) {
  let position = 0;
  
  function step() {
    position += 1;
    element.style.transform = `translateX(${position}px)`;
    
    if (position < 100) {
      requestAnimationFrame(step);
    }
  }
  
  requestAnimationFrame(step);
}

DocumentFragment を使用したバッチDOM操作

複数のDOM要素を追加する場合は、DocumentFragmentを使用してバッチ処理を行います。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 悪い例:個別にDOMに追加(追加ごとにリフロー発生の可能性)
function addItemsBad(items) {
  const list = document.getElementById('list');
  items.forEach(item => {
    const li = document.createElement('li');
    li.textContent = item;
    list.appendChild(li); // 毎回DOMに追加
  });
}

// 良い例:DocumentFragmentを使用
function addItemsGood(items) {
  const list = document.getElementById('list');
  const fragment = document.createDocumentFragment();
  
  items.forEach(item => {
    const li = document.createElement('li');
    li.textContent = item;
    fragment.appendChild(li); // メモリ上のフラグメントに追加
  });
  
  list.appendChild(fragment); // 一度だけDOMに追加
}

GPU合成を活用したアニメーション最適化

transformopacityプロパティを使用したアニメーションは、GPUで処理されるため、60fpsのスムーズなアニメーションを実現できます。

transform を使用した移動アニメーション

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/* 悪い例:left/topを使用(リフロー発生) */
.move-bad {
  position: absolute;
  transition: left 0.3s, top 0.3s;
}

.move-bad:hover {
  left: 100px;
  top: 100px;
}

/* 良い例:transformを使用(コンポジットのみ) */
.move-good {
  transition: transform 0.3s;
}

.move-good:hover {
  transform: translate(100px, 100px);
}

will-change による最適化ヒント

will-changeプロパティを使用して、ブラウザに最適化のヒントを与えることができます。

1
2
3
4
5
6
7
8
9
/* アニメーション開始前にレイヤーを準備 */
.animated-element {
  will-change: transform;
}

/* アニメーション後は will-change を解除 */
.animated-element.animation-done {
  will-change: auto;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// JavaScriptで動的にwill-changeを制御
function optimizeAnimation(element) {
  // アニメーション開始前に will-change を設定
  element.style.willChange = 'transform';
  
  // アニメーション完了後に解除
  element.addEventListener('transitionend', () => {
    element.style.willChange = 'auto';
  }, { once: true });
}

注意:will-changeはメモリを消費するため、必要な要素にのみ使用し、アニメーション完了後は解除することを推奨します。

パフォーマンス比較

以下の表は、異なるアニメーション手法のパフォーマンス比較です。

アニメーション手法 リフロー リペイント GPU合成 推奨度
left/top 発生 発生 発生 非推奨
margin 発生 発生 発生 非推奨
transform なし なし 発生 推奨
opacity なし なし 発生 推奨

Chrome DevTools を使用したパフォーマンス計測

Chrome DevToolsのPerformanceパネルを使用して、レンダリングパフォーマンスを計測できます。

Performance パネルの使い方

  1. DevToolsを開く(F12 または Ctrl+Shift+I)
  2. Performanceタブを選択
  3. 録画ボタンをクリックしてプロファイリングを開始
  4. 計測したい操作を行う
  5. 停止ボタンをクリック

確認すべき指標

指標 説明 目標値
Scripting JavaScript実行時間 フレームあたり16ms以下
Rendering レイアウト計算時間 最小化
Painting ペイント時間 最小化
FPS フレームレート 60fps

Rendering タブの活用

DevToolsのRenderingタブでは、ペイントやレイヤーを視覚化できます。

  1. DevToolsで「Rendering」パネルを開く(Ctrl+Shift+P → 「Show Rendering」)
  2. 「Paint flashing」を有効化してペイント領域を確認
  3. 「Layout Shift Regions」でレイアウトシフトを可視化
  4. 「Layer borders」でコンポジットレイヤーを確認

実践的な最適化チェックリスト

以下のチェックリストを参考に、Webページのレンダリングパフォーマンスを最適化してください。

クリティカルレンダリングパス

  • クリティカルCSSをインライン化している
  • 非クリティカルCSSは非同期で読み込んでいる
  • JavaScriptにはasyncまたはdefer属性を付与している
  • <link rel="preload">でクリティカルリソースを事前読み込みしている

リフローの最小化

  • レイアウト情報の読み取りと書き込みを分離している
  • requestAnimationFrameを使用してDOM更新をバッチ処理している
  • DocumentFragmentを使用して複数要素の追加をバッチ処理している
  • レイアウトを引き起こすプロパティの変更を最小化している

アニメーション最適化

  • アニメーションにはtransformopacityを使用している
  • left/top/marginではなくtransformで要素を移動している
  • 必要に応じてwill-changeを使用している
  • アニメーション完了後はwill-changeを解除している

画像とリソース

  • 適切なサイズの画像を使用している
  • 遅延読み込み(loading="lazy")を活用している
  • 画像のアスペクト比を指定してCLSを防止している

まとめ

ブラウザレンダリングの仕組みを理解することで、パフォーマンス最適化のための効果的な対策を講じることができます。

主要なポイントを振り返りましょう。

  1. レンダリングパイプライン:DOM構築 → CSSOM構築 → レンダーツリー → レイアウト → ペイント → コンポジットの6ステップで画面が描画される
  2. クリティカルレンダリングパス:クリティカルCSSのインライン化、非同期CSS/JSの読み込みで最適化
  3. リフローの最小化:読み取りと書き込みの分離、requestAnimationFrameの活用、DocumentFragmentの使用
  4. GPU合成の活用:アニメーションにはtransformopacityを使用してスムーズな60fpsを実現

これらの知識を活かして、高速で快適なWebページを構築してください。

参考リンク