Skip to content

ブラウザレンダリングパイプライン

🎯 核心問題

なぜあるウェブページは滑らかに動き、あるページはパワポのようにカクつくのか? ブラウザはどうやってHTML、CSS、JavaScriptのコードを、あなたが見ているウェブページに変換しているのか?本章では、ブラウザの「工場」内部に深く入り込み、そのワークフローを理解することで、よりパフォーマンスの高いウェブページを書けるようになる。

この記事で学べること:

章节内容学完能干嘛
第1章レンダリングパイプラインを理解する理由パフォーマンス最適化の必要性を理解する
第2章レンダリングパイプラインの5つの段階ブラウザレンダリングの基本フローを把握する
第3章DOMツリーとCSSOMツリーの構築HTMLとCSSの解析方法を理解する
第4章レンダーツリーの構築どの要素がレンダリングされるかを知る
第5章レイアウトとリフロー高コストなレイアウト計算を回避する
第6章ペイントとリペイント不要なペイント操作を減らす
第7章合成とGPUアクセラレーションGPUを活用してアニメーションパフォーマンスを向上させる
第8章イベントループJavaScriptの実行メカニズムを理解する
第9章パフォーマンス最適化の実践よく使われるパフォーマンス最適化テクニックを習得する

各章は「原理の理解」から始まり、最適化コードを手書きできる必要はない。パフォーマンス問題に遭遇したときに、いつでも参照しに来ればよい。


1. なぜ「レンダリングパイプライン」を理解する必要があるのか?

1.1 「動く」から「速く動く」へ:フロントエンド開発の進化の道

フロントエンドを学び始めた頃は、コードが「動くかどうか」だけを気にする——ページが表示され、ボタンがクリックできれば成功だ。しかしプロジェクトが大きくなり、ユーザーが増えると、すぐに残酷な現実に気づく:同じ機能でも、ある人が書いたページは絹のように滑らかで、別の人が書いたページはユーザーがマウスを投げつけたくなるほどカクつく

これは車の運転を学ぶようなものだ。初心者は「車が動くかどうか」だけを気にするが、ベテランドライバーは「いつギアを変えるか、いつブレーキを踏むか、どう運転すれば燃費が良いか」を気にする。ブラウザはあなたが運転する「車」であり、その「動作特性」を理解してこそ、速く安定して走らせることができる。

🐢 初心者の考え方(機能だけを重視)

  • ページが表示されればそれでいい
  • カクつきはブラウザの問題
  • パフォーマンス最適化は後回しでいい

🚀 上級者の考え方(体験を重視)

  • 滑らかさはユーザー体験の核心
  • ブラウザのワークフローを理解する
  • コードを書く時点でパフォーマンスを考慮する

レンダリングパイプラインを理解することは、「動く」から「速く動く」への重要な一歩だ。

1.2 実際の失敗談:「最適化」したのになぜか逆に遅くなった?

張さんのパフォーマンス失敗談

張さんはあるEC企業のフロントエンドエンジニアで、商品詳細ページの最適化を担当していた。このページは商品情報を表示する際にひどくカクつき、ユーザーからのクレームが絶えなかった。

張さんは考えた:「ページがカクつくのはDOMが多すぎるからだ。まずdisplay:noneで非表示にして、修正が終わってから表示すれば、ブラウザは何度もレンダリングしなくて済むはずだ」

そこで彼はこんなコードを書いた:

javascript
// あなたが考える「最適化」
const container = document.getElementById('list')
container.style.display = 'none'  // 先に非表示にすれば、レンダリングは発生しないはず?

for (let i = 0; i < 1000; i++) {
  const item = document.createElement('div')
  item.style.width = Math.random() * 100 + 'px'  // ランダムな幅
  container.appendChild(item)
}

container.style.display = 'block'  // 最後に表示、一括レンダリング

テストの結果、ページはさらにカクついた!張さんは困惑した:明らかに「最適化」したのに、なぜ逆に遅くなったのか?

後でフロントエンドリーダーがコードを見て、問題点を指摘した:要素は非表示でも、style.widthを変更するたびにブラウザのスタイル計算とレイアウトマークが発生し、ブラウザはバックグラウンドで大量の無駄な処理をしていたのだ。

正しい方法はDocumentFragmentを使ってメモリ上でバッチ操作し、最後に一度だけDOMに挿入して、レンダリングを1回だけトリガーすることだ。

💡 核心的教訓

ブラウザのワークフローを理解していなければ、「賢いつもり」で「最適化コード」を書いても、逆にパフォーマンスを悪化させてしまう。レンダリングパイプラインを理解してこそ、どの操作が高コストで、どの操作が低コストなのかがわかり、間違った場所で力を入れることを避けられる。


2. 核心概念:「レンダリングパイプライン」とは?

🤔 「レンダリング」とは?

レンダリング(Rendering)とは、簡単に言えばブラウザがコードを「描いて」あなたが見るウェブページに変換するプロセスだ。

これは印刷所で本を印刷する様子に例えられる:

  • HTML = 原稿の内容(文字、画像、章立て)
  • CSS = 組版指示(フォントサイズ、色、余白)
  • JavaScript = 動的修正(著者がその場で原稿を修正し、組版を調整する)

ブラウザはこれらの「材料」を受け取ると、いくつもの「工程」を経て、最終的にあなたが見るウェブページを「印刷」する。この一連の工程が、レンダリングパイプライン(Rendering Pipeline)だ。

より理解しやすくするために、パン屋を例にブラウザのレンダリングフローを説明しよう。

2.1 パン屋の例えでレンダリングパイプラインを理解する

あなたがパン屋を経営していて、毎日様々なパンを顧客のために作ると想像してほしい。このプロセスに含まれる各段階は、ブラウザのレンダリングフローと驚くほど似ている:

段階🥖 パン屋の例えブラウザの実際の動作具体例
1. 材料の準備材料リストを整理する(小麦粉、卵、クリーム…)DOMツリーの構築:HTMLをツリー構造に解析する<div><p>Hello</p></div>と書くと、ブラウザはdiv→p→"Hello"のツリーに解析する
2. レシピの準備レシピカードを整理する(各パンの材料比率)CSSOMツリーの構築:CSSをルールツリーに解析する.title { color: red }と書くと、ブラウザは「.titleの文字は赤色」と記録する
3. 計画を立てる材料とレシピに基づいて、今日作るパンを決めるレンダーツリーの構築:DOMとCSSOMを統合し、可視要素だけを残す<script>タグは表示されないので、レンダーツリーには含まれない
4. 配置を決めるパンをショーケースに並べ、各パンの位置を決めるレイアウト(Layout):各要素のサイズと位置を計算する「このdivは幅200px、高さ100px、画面の(50, 50)の位置にある」と計算する
5. 色付けと装飾パンに卵液を塗り、ゴマを振り、クリームを絞るペイント(Paint):要素の色、枠線、影などを「描く」「赤い文字」を実際に画面に描く
6. 組み立て完了すべてのパンを重ねて、美しく盛り付ける合成(Composite):複数のレイヤーを統合して最終画面にするGPUが背景レイヤー、文字レイヤー、画像レイヤーを統合して1枚の完全な画面にする

📊 この表から何が読み取れるか?

表を一行ずつ読み解き、レンダリングパイプラインの各段階を理解しよう:

段階1-2(準備段階):ブラウザはまずコードを「理解」する。HTMLとCSSは別々に解析される。なぜなら役割が異なるからだ——HTMLは「何があるか」を決め、CSSは「どのように見えるか」を決める。

段階3(統合段階):なぜ「統合」が必要か?すべてのHTML要素が表示されるわけではないからだ(例:<head><script>)。ブラウザは「可視要素」と「そのスタイル」を組み合わせて、「施工図」を作成する。

段階4-5(描画段階):レイアウトは「位置を計算」し、ペイントは「色を塗る」。レイアウトの変更(例:幅の変更)はペイントを引き起こすが、ペイントの変更(例:色の変更)はレイアウトを引き起こさない。

段階6(合成段階):現代ブラウザの「魔法」。従来の方式は「一度に全部描く」(CPUで逐次処理、遅い)。現代の方式は「レイヤー別描画+GPU合成」(速い)。これがtransformアニメーションがwidthアニメーションより滑らかな理由だ。

2.2 レンダリングパイプラインの5つの段階

🏭渲染管线从代码到像素的五步旅程
想象你在印刷厂工作:稿件要排版、印刷、装订,最后才能变成书本。浏览器渲染网页也一样,HTML 和 CSS 要经过一道道"工序",才能变成屏幕上的画面。
🌲
构建DOM/CSSOM
解析代码
🎨
构建渲染树
合并筛选
📐
布局
计算位置
✏️
绘制
填充颜色
🔮
合成
合并图层
👆 点击上方任意阶段,查看详细解释
💡核心思想:每个阶段各司其职,前面的阶段为后面阶段准备数据。理解这个流程,你就能知道什么时候用什么方式修改页面,才能避免性能问题。

3. 第一段階:DOMツリーとCSSOMツリーの構築

3.1 なぜ「ツリー化」するのか?

🤔 DOMとは?

DOM(Document Object Model、ドキュメントオブジェクトモデル)は、ブラウザがHTML文書を変換して作るツリー構造で、JavaScriptがページ要素を操作しやすくするためのものだ。

これは家系図に例えられる:

  • 最上位は「祖先」(<html>
  • その下は「子孫」(<body><head>
  • さらに下は「孫」(<div><p><span>

なぜツリーに変換するのか? ツリー構造は「検索」と「変更」が非常に効率的だからだ。例えば「classがtitleの要素をすべて見つけたい」場合、ブラウザはツリー上を素早く検索でき、バラバラのテキストの中から少しずつ探す必要がない。

ブラウザはHTMLを受け取ると、すぐに表示するのではなく、まずそれを「理解」する。このプロセスは3つのステップに分かれる:

ステップ1:字句解析——コードを「単語」に分解する

html
<div class="container">
  <p>Hello World</p>
</div>

ブラウザはこのコードを見ると、まず「単語分割」を行う:

  • <div> → 「開始タグ div」
  • class="container" → 「属性 class、値 container」
  • <p> → 「開始タグ p」
  • Hello World → 「テキスト内容」
  • </p> → 「終了タグ p」
  • </div> → 「終了タグ div」

ステップ2:構文解析——「単語」を「ノード」に組み立てる

ブラウザはHTMLのルールに従って、これらの「単語」を「ノード」に組み立てる:

  • 要素ノード:<div><p>
  • 属性ノード:class="container"
  • テキストノード:"Hello World"

ステップ3:ツリー構築——「親子関係」を確立する

最後に、ブラウザはタグのネスト関係に基づいてツリー構造を構築する:

Document(ドキュメントルートノード)
└── html
    └── body
        └── div.class = "container"
            └── p
                └── "Hello World"

3.2 CSSOMツリー:スタイルの「ルールブック」

🤔 CSSOMとは?

CSSOM(CSS Object Model、CSSオブジェクトモデル)は、ブラウザがCSSルールを変換して作るツリー構造で、各要素の最終的なスタイルを計算するために使われる。

これは服装コーディネートガイドに例えられる:

  • 上位のルール(bodyのフォント)は下位(すべての子要素)に影響する
  • 衝突がある場合(同じ要素に複数のルールが異なる色を指定)、「優先度」に従ってどれを使うか決める
  • 最終的に各要素がどんな「服」を着るべきかを計算する

CSSOMの構築プロセスはDOMと似ているが、1つ重要な違いがある:CSSは「継承」と「カスケード」があることだ。

CSSOMの構築プロセスを見る

元のCSS:

css
body {
  font-size: 16px;
  color: #333;
}

.container {
  width: 100%;
  color: red;  /* bodyのcolorを上書きする */
}

.container p {
  font-weight: bold;
}

構築後のCSSOMツリー:

StyleSheet
├── body
│   ├── font-size: 16px
│   └── color: #333
└── .container
    ├── width: 100%
    ├── color: red  (優先度が高いため、bodyのcolorを上書き)
    └── p
        └── font-weight: bold

3.3 失敗談:なぜCSSが「効かない」のか?

落とし穴1:CSSセレクタの詳細度の衝突

よくある間違いを見る
css
/* あなたが書いたCSS */
#header { color: red; }      /* idセレクタ、詳細度100 */
.title { color: blue; }     /* classセレクタ、詳細度10 */

/* HTML */
<div id="header" class="title">この文字は何色?</div>

あなたは青だと思うが、結果はだ。なぜならidセレクタの詳細度(100)がclassセレクタの詳細度(10)より高いからだ。

落とし穴2:HTMLタグが閉じられていない、ブラウザが「自動修正」する

ブラウザがエラーのあるHTMLをどう修正するかを見る
html
<!-- あなたが書いたHTML -->
<div>
  <p>これは文章です
</div>

<!-- ブラウザが修正した後 -->
<div>
  <p>これは文章です</p>  <!-- ブラウザが自動的にタグを閉じてくれる -->
</div>

ブラウザはとても「寛容」で、あなたのミスを自動修正してくれる。しかしこの寛容さには代償がある——ブラウザはあなたの意図を推測するために追加の計算が必要で、パフォーマンスに影響する

🌲DOM到渲染树浏览器如何构建渲染树
浏览器需要把 HTML 和 CSS 合并成一棵"渲染树"。想象你在组装家具:图纸是 DOM,说明书是 CSSOM,只有结合两者,才能知道每个零件长什么样、放在哪里。
DOM树
<html>
<head>
<style>
<body>
<div>
<span>
<script>
+
CSSOM树
body
div
color: red
span
display: block
script
display: none
=
渲染树
div
span
可见节点
不可见节点(不包含在渲染树中)
💡核心要点:渲染树只包含可见的节点(display: none 的元素会被忽略)。每个渲染树节点都包含对应的 DOM 节点和计算出的样式信息。渲染树构建完成后,浏览器才能进入布局阶段。

4. 第二段階:レンダーツリーの構築

4.1 なぜ「レンダーツリー」が必要なのか?

あなたはこう疑問に思うかもしれない:「すでにDOMツリーとCSSOMツリーがあるのに、なぜさらにレンダーツリーを構築する必要があるのか?DOMをそのまま使えばいいのでは?」

答えは:DOMツリーには「無駄な」情報が多すぎるからだ。

例えば次のHTMLを見てほしい:

html
<html>
<head>
  <title>ページタイトル</title>
  <style>/* CSSコード */</style>
  <script>/* JavaScriptコード */</script>
</head>
<body>
  <div class="container">
    <p>可視コンテンツ</p>
  </div>
  <div style="display: none">
    <p>非表示コンテンツ(display:none)</p>
  </div>
</body>
</html>

DOMツリーはすべての要素を含む

  • <head><title><style><script>(これらは表示されない)
  • display: noneのdiv(これも表示されない)

しかしレンダーツリーは「画面に描くべき」要素だけを含む

  • <head>とその子要素を除去
  • display: noneのdivを除去

4.2 レンダーツリーの構築ルール

ブラウザはレンダーツリーを構築する際、一連のルールに従う:

シーン処理方法パフォーマンスへの影響
display: noneレンダーツリーから完全に除外要素とその子要素がすべて不可視✅ レンダリング作業を削減
visibility: hiddenレンダーツリーに含まれるが、描画されないスペースを占有するが、完全に透明⚠️ レイアウト計算は必要
opacity: 0レンダーツリーに含まれるが、透明インタラクション可能(クリックできる)が、見えない⚠️ レイアウト計算は必要
ビューポート外レンダーツリーに含まれるが、一時的に描画されないビューポートにスクロールされたときに描画⚠️ ただしレンダーツリー内には存在

📊 この表から何が読み取れるか?

重要な発見display: noneは唯一「本当にパフォーマンスを節約できる」非表示方法だ。要素が完全にレンダーツリー内に存在しないため、ブラウザはレイアウトやペイントの処理を一切行わない。

一方、visibility: hiddenopacity: 0は「見えない」が、レンダーツリー内に存在し続けるため、ブラウザはレイアウト計算(スペース占有)が必要だ。「非表示だがレイアウトに影響を与えたくない」場合(フェードイン/アウトアニメーションなど)はopacityを使い、「完全に非表示にしてスペースも占有しない」場合はdisplay: noneを使う。

4.3 失敗談:display:noneを設定したのに、なぜページがまだカクつくのか?

❌ よくある誤解:display:noneの要素は「存在しない」と思い込む

多くの人はdisplay: noneを設定すると要素が「消滅」し、どう操作してもパフォーマンスに影響しないと思っている。これは間違いだ!

display: noneの要素はレンダーツリー内に存在しないが、JavaScriptでその属性を変更すると、ブラウザは以下を行う必要がある:

  1. スタイルの再計算(CSSルールのマッチング)
  2. 変更の追跡(将来の表示に備える)

次の「最適化」例を見てほしい:

「無効な最適化」のコードを見る
javascript
// ❌ あなたが考える「最適化」:先に非表示にして、修正が終わってから表示
const container = document.getElementById('list')
container.style.display = 'none'

// DOMを激しく操作
for (let i = 0; i < 1000; i++) {
  const item = document.createElement('div')
  item.style.width = Math.random() * 100 + 'px'  // 幅を変更!
  item.textContent = `Item ${i}`
  container.appendChild(item)
}

container.style.display = 'block'

// 問題:style.widthを変更するたびに、ブラウザはスタイルを再計算する必要がある
// 要素がdisplay:noneでも!

✅ 正しい最適化の方法:

javascript
// DocumentFragmentを使ってバッチ操作
const container = document.getElementById('list')
const fragment = document.createDocumentFragment()  // 仮想コンテナ

// すべての操作をメモリ上のfragmentで行う
for (let i = 0; i < 1000; i++) {
  const item = document.createElement('div')
  item.style.width = Math.random() * 100 + 'px'
  item.textContent = `Item ${i}`
  fragment.appendChild(item)  // 実際のDOMには影響しない
}

// 一度だけ実際のDOMに挿入、レンダリングは1回だけトリガー
container.appendChild(fragment)

5. 第三段階:レイアウトとリフロー

5.1 「レイアウト」とは?

🤔 レイアウト(Layout)とは?

レイアウトは、リフロー(Reflow)とも呼ばれ、ブラウザがレンダーツリー内の各要素について「どの位置に、どれだけのスペースを占めるか」を計算するプロセスだ。

これはインテリアデザイナーが部屋を採寸する様子に例えられる:

  • まず各部屋の縦横を測る
  • 家具をどこに置くか決める
  • 各家具の座標を計算する

なぜレイアウトは「高コスト」なのか? 1つの要素の変更が他の要素に影響を与える可能性があるからだ。例えばdivを広げると、隣のdivが押し下げられ、ページ全体の再計算が必要になるかもしれない。

5.2 リフローを引き起こす「地雷原」

以下はリフローを引き起こすよくある操作だ。ブックマークして暗記することを推奨する

カテゴリプロパティ/操作パフォーマンス影響代替案
サイズwidth, height, min/max-width/height💀💀💀transform: scale()を使う
位置top, right, bottom, left💀💀💀transform: translate()を使う
余白margin, padding💀💀transformまたはgapを使う
枠線border-width💀💀頻繁な変更を避ける
内容テキスト内容の変更、画像の読み込み💀💀スペースを事前確保し、レイアウトシフトを避ける
フォントfont-size, line-height💀💀💀頻繁な変更を避ける
表示display値の変更💀💀💀visibilityまたはopacityを使う(完全非表示が必要ない場合)
クエリoffsetWidth, offsetHeightなど💀💀💀💀💀バッチ読み取りで、レイアウトスラッシングを避ける

📊 この表から何が読み取れるか?

重要な発見

  1. ジオメトリプロパティ(幅、高さ、位置)が最も高コスト:完全なレイアウト計算を引き起こす
  2. クエリプロパティは変更よりも危険offsetWidthの読み取りは強制同期レイアウトを引き起こす(5.4節参照)
  3. transformとopacityが最もパフォーマンスが良い:リフローを引き起こさず、合成のみをトリガーする

5.3 失敗談:なぜアニメーションがパワポのようにカクつくのか?

落とし穴:widthでアニメーションする

パフォーマンスの悪いアニメーションコードを見る
css
/* ❌ 悪いアニメーション:リフローを引き起こす */
.box {
  width: 100px;
  transition: width 0.3s;
}

.box:hover {
  width: 200px;  /* 幅の変更はリフローを引き起こす! */
}

アニメーションの各フレームでリフローが発生し、ブラウザは以下を行う必要がある:

  1. 幅の再計算
  2. 位置の再計算(他の要素に影響する可能性あり)
  3. 再ペイント

✅ 良いアニメーション:transformを使う

css
/* ✅ 良いアニメーション:合成のみをトリガー */
.box {
  width: 100px;
  transform: scaleX(1);
  transition: transform 0.3s;
}

.box:hover {
  transform: scaleX(2);  /* 拡大縮小はリフローを引き起こさない! */
}

transformはGPUが直接処理し、リフローとリペイントを引き起こさず、アニメーションは絹のように滑らかだ。

5.4 パフォーマンスキラー:強制同期レイアウト

💀 最も危険なパフォーマンス問題:レイアウトスラッシング

強制同期レイアウト(Forced Synchronous Layout)は、レイアウトスラッシング(Layout Thrashing)とも呼ばれ、最も一般的で深刻なパフォーマンス問題だ。

その原因は:JavaScriptがレイアウトプロパティ(offsetWidthなど)を読み取る際、ブラウザは正確な値を返すために即座にレイアウト計算を実行しなければならない

「読み取りと書き込みを交互に」行うと、ブラウザは「レイアウト→読み取り→レイアウト→読み取り」を繰り返し、悪循環に陥る。

レイアウトスラッシングのコードを見る
javascript
// ❌ 極めて悪い:読み書き交互でレイアウトスラッシングを引き起こす
const elements = document.querySelectorAll('.item')

for (let i = 0; i < elements.length; i++) {
  const height = elements[i].offsetHeight  // 読み取り → 強制レイアウト
  elements[i].style.width = (height * 2) + 'px'  // 書き込み → リフローが必要とマーク
  // 次のループの読み取りでまた強制レイアウト…悪循環!
}

// 100個の要素があれば、100回のレイアウト計算が発生する!

✅ 正しい最適化方法:読み取りと書き込みを分離

javascript
const elements = document.querySelectorAll('.item')

// ステップ1:バッチ読み取り(まず全部読み取る)
const heights = []
for (let i = 0; i < elements.length; i++) {
  heights.push(elements[i].offsetHeight)  // レイアウトは1回だけ
}

// ステップ2:バッチ書き込み(その後全部書き込む)
requestAnimationFrame(() => {
  for (let i = 0; i < elements.length; i++) {
    elements[i].style.width = (heights[i] * 2) + 'px'  // リフローは1回だけ
  }
})
📐布局与重排看看布局计算如何影响页面
盒子
邻居元素
触发阶段:
性能影响:-
是否影响其他元素:-
💡核心要点:布局属性(如 width、margin)会触发重排,影响周围元素的位置。而 transform 只触发合成,在 GPU 上处理,不影响其他元素,性能更好。

6. 第四段階:ペイントとリペイント

6.1 「ペイント」とは?

🤔 ペイント(Paint)とは?

ペイントは、ブラウザが「レイアウト計算済み」の要素を実際に画面に「描く」プロセスだ。

これは部屋の壁を塗る様子に例えられる:

  • レイアウト段階 = 寸法を測り、線を引く
  • ペイント段階 = 実際に塗料を塗り、壁紙を貼る

ペイントはレイアウトほど高コストではないが、安くもない。 頻繁なペイントは依然としてパフォーマンスに影響する。特に複雑な要素(影、グラデーションなど)は注意が必要だ。

6.2 リペイントを引き起こすシグナル

リフローと異なり、リペイントは「外観」の変更のみを含み、「ジオメトリ」の変更は含まない:

カテゴリプロパティパフォーマンス影響備考
color, background-color💀最も一般的なリペイントトリガー
背景background-image, background-position💀💀画像は単色より遅い
枠線border-color, border-style💀枠線の色/スタイルの変更
文字text-decoration, text-shadow💀💀影は単なる文字より遅い
ボックスシャドウbox-shadow💀💀💀複雑な影は非常に遅い
角丸border-radius💀角丸のサイズ変更
透明度opacity特殊:リペイントを引き起こさず、合成のみをトリガー

📊 この表から何が読み取れるか?

重要な発見opacityは特殊だ!transformと同様に、リペイントを引き起こさず、直接合成段階をトリガーする。これがopacityを使ったフェードイン/アウトアニメーションのパフォーマンスが最も良い理由だ。

また、影とグラデーションはリペイントの中でも特に高コストだ。複雑なピクセル計算が必要だからだ。ページにbox-shadowが多い場合は、疑似要素や画像での代替を検討しよう。

6.3 失敗談:なぜhover効果がカクつくのか?

落とし穴:box-shadowでhoverアニメーションをする

パフォーマンスの悪いhover効果を見る
css
/* ❌ 悪いhover効果:box-shadowアニメーションは非常に遅い */
.card {
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  transition: box-shadow 0.3s;
}

.card:hover {
  box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);  /* 影は非常に遅い! */
}

box-shadowはピクセル単位の計算が必要で、アニメーション時にカクつく。

✅ 良い方法:transformまたは疑似要素を使う

css
/* ✅ 良いhover効果:transformを使う */
.card {
  transform: translateY(0);
  transition: transform 0.3s, box-shadow 0.3s;
}

.card:hover {
  transform: translateY(-4px);  /* hover時のみ影を変更、アニメーションはしない */
  box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
}
🎨绘制层优化浏览器如何通过分层提升性能
🖼️背景层
📄内容层
卡片
触发新层的 CSS 属性:
transform: translate3d(0,0,0)任何3D变换都会创建新层
opacity配合transition使用时
position: fixed固定定位元素需要独立层
will-change: transform显式提示浏览器创建层
💡核心要点:浏览器把需要动画的元素提升到独立的 GPU 层,这样动画时只需要调整位置和透明度,不需要重绘。但不要滥用,每个层都会占用 GPU 内存。

7. 第五段階:合成とGPUアクセラレーション

7.1 「合成」とは?

🤔 合成(Composite)とは?

合成は、現代ブラウザの「魔法」だ。ページの異なる部分を複数のレイヤー(Layer)に分割し、GPU(グラフィックスプロセッサ)を使って並列に最終画面を合成する。

これはPhotoshopのレイヤーに例えられる:

  • 従来の方式 = すべてを1つのレイヤーに描く(CPU逐次処理、遅い)
  • 合成方式 = レイヤー別に描き、最後に統合(GPU並列処理、速い)

なぜ合成は速いのか? GPUは「画像合成」のような並列タスクが得意で、CPUより数十倍速いからだ。

7.2 どの要素が「合成レイヤー」に昇格されるのか?

ブラウザは特定の要素を自動的に独立した合成レイヤーに昇格させる。以下はよくあるトリガー条件だ:

トリガー条件CSSプロパティ/値パフォーマンス影響注意事項
3D変形transform: translate3d(), rotate3d()✅✅✅アニメーションパフォーマンスが最高
ハードウェアアクセラレーションhacktransform: translateZ(0)✅✅通称「強制GPUアクセラレーション」
透明度アニメーションopacityの変化(アニメーションと併用)✅✅✅リペイントを引き起こさない
固定配置position: fixedスクロール時の繰り返しレイアウトを回避
Will-Changewill-change: transform, opacity✅✅事前にレイヤーを作成、メモリに注意
Canvas/WebGL<canvas>, WebGLコンテンツ✅✅デフォルトで独立レイヤー
Video<video>✅✅独立レイヤー、相互影響を防止

📊 この表から何が読み取れるか?

重要な発見transformopacityは最もパフォーマンスの良いアニメーションプロパティだ。リフローとリペイントを引き起こさず、直接合成をトリガーするからだ。これがパフォーマンス最適化ガイドで常に「transformとopacityでアニメーションせよ」と言われる理由だ。

ただし注意:各合成レイヤーはGPUメモリを消費するtranslateZ(0)の乱用はメモリ爆発を引き起こす(7.4節参照)。

7.3 失敗談:合成レイヤーが多すぎて逆にカクつく?

💀 過剰最適化の罠

「GPUアクセラレーションは速い」と聞いて、すべての要素にtransform: translateZ(0)を追加した結果、ページが逆にさらにカクついたという人がいる。

問題の原因: 各合成レイヤーはGPUに「テクスチャ」(ビットマップ)を保存する必要があり、メモリを消費する。ページに100個の合成レイヤーがあると、GPUメモリがパンクし、低スペックデバイスではクラッシュしたりCPUレンダリングにフォールバックしたりする。

「過剰最適化」のコードを見る
css
/* ❌ 間違ったやり方:すべての要素にGPUアクセラレーションを有効にする */
.card { transform: translateZ(0); }
.button { transform: translateZ(0); }
.icon { transform: translateZ(0); }
/* ... 100個の要素すべてに追加 ... */

/* 結果:GPUメモリが爆発し、ページがフリーズ */

✅ 正しいやり方:必要なときだけ使う

css
/* 戦略1:本当にアニメーションが必要な要素だけに有効にする */
.card {
  transition: transform 0.3s ease;
}

.card:hover {
  transform: translateY(-5px);  /* 自動的に合成レイヤーが作成される */
}

/* 戦略2:will-changeでブラウザにヒントを与える */
.card {
  will-change: transform;  /* 事前にレイヤーを作成 */
}

/* 戦略3:アニメーション終了後に解除 */
.card:not(:hover) {
  will-change: auto;  /* GPUメモリを解放 */
}
🎬合成层演示浏览器渲染的最后阶段 - 图层合成
合成是浏览器渲染的最后一步。想象你在制作PPT动画:你已经准备好了所有图层,现在只需要调整它们的位置、透明度,然后把它们叠在一起显示出来。这就是合成要做的事情。
🖼️背景层
📄内容层
浮层
合成结果
🖼️
📄
💡核心要点:合成阶段在 GPU 上执行,只调整位置、透明度等,不重新绘制像素。因此 transform 和 opacity 动画性能最好,不会触发重排和重绘。

8. イベントループ:JavaScriptの「分身の術」

🤔 イベントループとは?

イベントループ(Event Loop)は、JavaScriptが「非同期」を実現するメカニズムだ。JavaScriptはシングルスレッド(一度に1つのことしかできない)だが、ユーザーのクリック、ネットワークリクエスト、タイマーなど複数のタスクを処理する必要がある。そのため、これらのタスクを管理する「スケジューリングシステム」が必要になる。

これは宅配便の仕分けセンターに例えられる:

  • Call Stack(コールスタック) = 現在処理中の荷物
  • Web APIs = 外部提携倉庫(タイマー、ネットワークリクエストなど)
  • Callback Queue(コールバックキュー) = 処理待ちの荷物棚
  • Event Loop(イベントループ) = 仕分けロボット(「次のタスクを処理できるか」を常にチェックしている)

8.1 マクロタスクとマイクロタスク

初期のJavaScriptは1つのタスクキューしか持っていなかった。しかし非同期プログラミングが複雑になるにつれ、ブラウザは2種類のタスクを導入した:

タイプよくある発生源優先度実行タイミング
マクロタスクsetTimeout/setInterval、I/O操作、UIレンダリング各イベントループサイクルで1つ実行
マイクロタスクPromise.thenMutationObserver現在のマクロタスク終了後、すべてのマイクロタスクを即座にクリア

実行順序の「口诀」

1. 現在のマクロタスクを実行(例:<script>全体)
2. 実行中に生成されたすべてのマイクロタスクを実行(Promise.thenなど)
   ↳ マイクロタスクは新しいマイクロタスクを生成でき、すべてクリアされるまで続く
3. 必要に応じてUIレンダリングを実行(リフロー/リペイント)
4. 次のイベントループサイクルを開始し、次のマクロタスクを実行

8.2 失敗談:PromiseはsetTimeoutより速い?

❌ よくある誤解:setTimeout(fn, 0)は「即座に」実行される

多くの人はsetTimeout(fn, 0)が「0ミリ秒後に即座に実行される」と思っているが、これは間違った理解だ。

実際には、setTimeout(fn, 0)の意味は:「少なくとも0ミリ秒待ってから、コールバックをマクロタスクキューに追加する」だ。しかし現在のコールスタックがクリアされ、マイクロタスクキューがクリアされ、可能なUIレンダリングが完了するまで待つ必要がある。

実行順序を見る
javascript
console.log('1. Start')

setTimeout(() => {
  console.log('2. setTimeout callback')
}, 0)

Promise.resolve().then(() => {
  console.log('3. Promise.then')
})

console.log('4. End')

// あなたが想像する出力順序:
// 1. Start
// 4. End
// 2. setTimeout callback  ← setTimeout(0)は即時じゃないの?
// 3. Promise.then

// 実際の出力順序:
// 1. Start
// 4. End
// 3. Promise.then         ← Promise.thenがsetTimeoutより先に実行される!
// 2. setTimeout callback

実行フロー図解:

コールスタック(Call Stack)     マクロタスクキュー               マイクロタスクキュー
                              [setTimeout callback]         [Promise.then callback]

1. console.log('1. Start')
   → 出力: 1. Start

2. setTimeout(fn, 0)
   → コールバックをマクロタスクキューに追加  ← [setTimeout callback]

3. Promise.resolve().then()
   → コールバックをマイクロタスクキューに追加                      ← [Promise.then callback]

4. console.log('4. End')
   → 出力: 4. End

5. コールスタックがクリアされ、マイクロタスクキューをチェック
   → Promise.thenコールバックを発見
   → 実行: console.log('3. Promise.then')
   → 出力: 3. Promise.then

6. マイクロタスクキューがクリアされる
   → UIレンダリングが必要な場合あり(変更があれば)

7. マクロタスクキューをチェック
   → setTimeoutコールバックを発見
   → 実行: console.log('2. setTimeout callback')
   → 出力: 2. setTimeout callback

💡 核心的教訓

マイクロタスクはマクロタスクより「緊急」だ。ある操作を「現在のコードブロック終了後、かつUI更新前」にできるだけ早く実行したい場合は、Promise.thenまたはqueueMicrotaskを使う。

setTimeout(0)は即時実行を保証せず、少なくとも現在のコールスタックがクリアされ、マイクロタスクキューがクリアされるまで遅延される。

Event Loop: How JavaScript Executes Code

Code queue

1
console.log("1")
Executing
2
setTimeout(() => console.log("2"), 0)
3
console.log("3")
4
fetch("/api").then(() => console.log("4"))
5
console.log("5")

Worker (single thread)

👨‍💻
Running
Run console.log("1")

Task queue

No pending tasks

Output log

Waiting for output...

Execution order: not started

Written order: 1, 2, 3, 4, 5

Code is written top to bottom, but it does not always run top to bottom because async work is delayed until the current code finishes.

🔄宏任务与微任务事件循环中的任务优先级
JavaScript 是单线程的,但可以通过任务队列实现异步。就像餐厅只有一个厨师,但他可以同时处理多个订单:先做VIP订单(微任务),再做普通订单(宏任务)。
主线程(执行栈)
同步代码
任务队列
微任务队列(优先级高)
Promise.then()
queueMicrotask()
宏任务队列(优先级低)
setTimeout()
setInterval()
I/O 操作
代码示例
console.log('1')

setTimeout(() => console.log('2'), 0)  // 宏任务

Promise.resolve().then(() => console.log('3'))  // 微任务

console.log('4')

// 输出顺序:1 → 4 → 3 → 2
💡核心要点:每次宏任务执行完后,会清空所有微任务,然后再执行下一个宏任务。这就是为什么 Promise.then() 比 setTimeout() 先执行。

9. パフォーマンス最適化の実践:ウェブページを「飛ばせ」

レンダリングパイプラインのワークフローを理解したところで、最適化の方法を見ていこう。以下は最も実用的な5つの最適化テクニックだ。

9.1 黄金律:強制同期レイアウトを避ける

問題:レイアウトプロパティの読み取りと書き込みを交互に行うと、レイアウトスラッシングが発生する。

最適化前後の比較を見る
javascript
// ❌ 極めて悪い:読み書き交互でレイアウトスラッシングを引き起こす
for (let i = 0; i < elements.length; i++) {
  const height = elements[i].offsetHeight  // 読み取り → 強制レイアウト
  elements[i].style.height = (height * 2) + 'px'  // 書き込み → リフローが必要とマーク
  // 次のループの読み取りでまた強制レイアウト…悪循環!
}

// ✅ 極めて良い:まず全部読み取り、その後全部書き込み
// ステップ1:バッチ読み取り
const heights = []
for (let i = 0; i < elements.length; i++) {
  heights.push(elements[i].offsetHeight)
}

// ステップ2:バッチ書き込み
requestAnimationFrame(() => {
  for (let i = 0; i < elements.length; i++) {
    elements[i].style.height = (heights[i] * 2) + 'px'
  }
})

9.2 transformとopacityでアニメーションする

問題widthheightlefttopでアニメーションするとリフローが発生する。

最適化前後の比較を見る
css
/* ❌ 悪いアニメーション:リフローを引き起こす */
.box {
  transition: width 0.3s, left 0.3s;
}
.box.moving {
  width: 200px;
  left: 100px;
}

/* ✅ 良いアニメーション:合成のみをトリガー */
.box {
  transition: transform 0.3s;
}
.box.moving {
  transform: translateX(100px) scaleX(2);
}

9.3 仮想スクロール:大量データリストの解決策

問題:リスト項目数が数千に達すると、DOMノード数が多すぎてパフォーマンス問題が発生する。

核心思想:ビューポート内の可視リスト項目のみをレンダリングする(少量のバッファを含む)。DOMノード数は固定され、データ総量とは無関係になる。

渲染性能优化让页面丝滑流畅的秘诀
渲染性能优化的目标是每秒60帧(16.67ms/帧)。就像拍电影,每秒帧数越多,画面越流畅。超过这个时间,用户就会感觉卡顿。
❌ 不好的做法
// 触发重排和重绘
function animate() {
element.style.width = '100px'
element.style.height = '100px'
requestAnimationFrame(animate)
}
性能开销
VS
✅ 优化做法
/* 只触发合成 */
function animate() {
element.style.transform = 'translate3d(0,0,0)'
requestAnimationFrame(animate)
}
性能开销
黄金法则:
1️⃣优先使用 transformopacity 做动画
2️⃣避免频繁读取布局属性(如 offsetWidth)
3️⃣使用 will-change 提前告知浏览器
💡核心要点:渲染路径越长,性能越差。最佳路径是:合成(Composite)> 重绘(Paint)> 布局(Layout)> 样式计算(Style)。尽量让动画停留在"合成"阶段,在 GPU 上完成。
仮想スクロールの実装を見る
vue
<template>
  <div class="virtual-list" @scroll="handleScroll">
    <!-- プレースホルダー要素、スクロールバーを支える -->
    <div class="phantom" :style="{ height: totalHeight + 'px' }"></div>

    <!-- 実際にレンダリングされるリスト項目 -->
    <div class="content" :style="{ transform: `translateY(${offsetY}px)` }">
      <div
        v-for="item in visibleItems"
        :key="item.id"
        class="item"
        :style="{ height: itemHeight + 'px' }"
      >
        {{ item.name }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'

const props = defineProps({
  items: Array,
  itemHeight: { type: Number, default: 50 }
})

const scrollTop = ref(0)
const buffer = 5  // バッファ数

// 可視領域に表示できる項目数
const visibleCount = computed(() => 10)

// 開始インデックス
const startIndex = computed(() =>
  Math.max(0, Math.floor(scrollTop.value / props.itemHeight) - buffer)
)

// 終了インデックス
const endIndex = computed(() =>
  Math.min(props.items.length, startIndex.value + visibleCount.value + buffer * 2)
)

// 現在可視のデータ
const visibleItems = computed(() =>
  props.items.slice(startIndex.value, endIndex.value)
)

// 総高さ
const totalHeight = computed(() => props.items.length * props.itemHeight)

// オフセット
const offsetY = computed(() => startIndex.value * props.itemHeight)

const handleScroll = (e) => {
  scrollTop.value = e.target.scrollTop
}
</script>

9.4 デバウンスとスロットル:イベント発火頻度を減らす

問題:頻繁に発火するイベント(scroll、resizeなど)がパフォーマンス問題を引き起こす。

デバウンスとスロットルの実装を見る
javascript
// デバウンス(Debounce):遅延実行。遅延時間内に再発火した場合、タイマーをリセット
function debounce(fn, delay) {
  let timer = null
  return function (...args) {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), delay)
  }
}

// スロットル(Throttle):固定時間間隔で実行
function throttle(fn, interval) {
  let lastTime = 0
  return function (...args) {
    const now = Date.now()
    if (now - lastTime >= interval) {
      lastTime = now
      fn.apply(this, args)
    }
  }
}

// 使用例
window.addEventListener('scroll', debounce(handleScroll, 200))
window.addEventListener('resize', throttle(handleResize, 100))

9.5 遅延読み込み:重要でないリソースの読み込みを遅延させる

問題:ファーストビューでリソースを読み込みすぎると、ページの表示が遅くなる。

遅延読み込みの実装を見る
javascript
// 画像の遅延読み込み
const lazyImages = document.querySelectorAll('img[data-src]')

const imageObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target
      img.src = img.dataset.src  // 実際の画像を読み込む
      img.removeAttribute('data-src')
      observer.unobserve(img)  // 監視を停止
    }
  })
})

lazyImages.forEach(img => imageObserver.observe(img))

10. あなたが今識別できるはずのパフォーマンス問題

ブラウザのレンダリングパイプラインを理解したことで、以下のよくあるパフォーマンス問題を識別できるようになったはずだ:

問題コード問題点AIへの伝え方
element.style.width = ...ループ内で頻繁に幅を変更している「ここで複数回のリフローが発生します。transformを使うかバッチ処理に変更してください」
height = element.offsetHeight書き込み直後にレイアウトプロパティを読み取っている「これは強制同期レイアウトです。読み取りと書き込みを分離してください」
element.className = ...頻繁なclass変更でスタイル再計算が発生「classList.add/removeに置き換えて、スタイル計算を減らしてください」
アニメーションにwidth/leftを使用リフローとリペイントを引き起こし、パフォーマンスが悪い「transformとopacityでアニメーションするように変更してください」
すべての要素にtranslateZ(0)を追加GPUアクセラレーションの乱用でメモリ爆発「アニメーションが必要な要素だけにGPUアクセラレーションを有効にしてください」
リスト項目10000個をすべてレンダリングDOMノードが多すぎてカクつく「仮想スクロールを実装し、可視領域だけをレンダリングしてください」
scrollイベント内で直接DOM操作発火頻度が高すぎてカクつく「requestAnimationFrameまたはスロットルで最適化してください」
box-shadowでhoverアニメーション複雑な影の計算が非常に遅い「transformまたは疑似要素に変更し、影のアニメーションを避けてください」

各章の「失敗談」をしっかり読んだなら、以下の核心概念も習得しているはずだ:

  • レンダリングパイプラインの5段階:DOM/CSSOM → レンダーツリー → レイアウト → ペイント → 合成
  • リフロー vs リペイント:リフローが最も高コスト(ジオメトリ変更)、リペイントはその次(外観変更)
  • 強制同期レイアウト:読み書き交互はレイアウトスラッシングを引き起こす。必ず分離すること
  • GPUアクセラレーション:transformとopacityはGPUが処理し、パフォーマンスが最良
  • イベントループ:JavaScriptはシングルスレッドで、タスクキューを通じて非同期を実現する

これらの概念はパフォーマンスのボトルネックを素早く特定するのに役立つ。

💡 パフォーマンス問題に遭遇したときはAIにこう伝えよう

  • 「アニメーションがカクつきます。リフローやリペイントが発生していないか確認してください」
  • 「スクロールパフォーマンスが悪いです。スロットルまたはrequestAnimationFrameが必要かもしれません」
  • 「リストのデータ量が多いときにカクつきます。仮想スクロールが必要です」
  • 「頻繁なスタイル変更でパフォーマンス問題が発生しています。transformで最適化してください」

11. まとめ:レンダリングパイプライン最適化の本質

本文の学習を通じて、以下の核心的結論が得られる:

実践から見ると:最適化は多ければ多いほど良いのではなく、「的を射た」最適化であるほど良い。ブラウザのレンダリングパイプラインを理解してこそ、どこに力を入れ、どこで手を抜くべきかがわかる。

コストの視点から見ると

  • パフォーマンスの浪費の大部分は、レイアウトプロパティの頻繁な読み書き交互に起因する。読み書き分離とバッチ処理で解決する必要がある
  • 複雑なアニメーション効果がリフローとリペイントを引き起こしている場合、多くの場合「間違ったプロパティ」を使っていることが原因で、transformopacityで解決する必要がある
  • 大量データのリストレンダリングでは、仮想DOMだけでは不十分で、仮想スクロールなどの技術と組み合わせる必要がある

目標は:与えられたブラウザとハードウェアの条件下で、すべてのレンダリングステップの投入が明確なパフォーマンスリターンを持つようにすることだ。


12. 用語对照表

英語用語日本語对照説明
DOMドキュメントオブジェクトモデルブラウザがHTML文書を解析して形成したツリー構造。JavaScriptはDOM APIを通じてページ要素を操作できる
CSSOMCSSオブジェクトモデルブラウザがCSSを解析して形成したツリー構造。DOMと組み合わせて最終スタイルを計算する
Render TreeレンダーツリーDOMツリーとCSSOMツリーを統合して作られ、可視ノードのみを含む。後続のレイアウト計算とペイントに使用される
Layoutレイアウトレンダーツリー内の各ノードのジオメトリ情報(位置、サイズ)を計算するプロセス。Reflow(リフロー)とも呼ばれる
Reflowリフロー要素のサイズ、位置などのジオメトリプロパティが変更されたとき、ブラウザがレイアウトを再計算するプロセス
Paintペイントレイアウト計算後の要素スタイル(色、背景、枠線など)を画面に描画するプロセス
Repaintリペイント要素の外観プロパティ(色、背景など)が変更されたがジオメトリプロパティに影響しない場合にトリガーされる描画更新
Composite合成複数の描画レイヤー(Layer)を最終的な画面画像に統合するプロセス。通常GPU上で実行される
Layerレイヤー/合成レイヤーブラウザがレンダリング最適化のために作成する独立した描画面。個別に変形・合成できる
Event LoopイベントループJavaScriptの非同期実行メカニズム。マクロタスクとマイクロタスクの実行をスケジューリングする
Call Stackコールスタック現在実行中のJavaScript関数を記録するデータ構造
Macro Taskマクロタスクイベントループで優先度が低いタスクタイプ。setTimeout、setInterval、I/O操作など
Micro Taskマイクロタスクイベントループで優先度が高いタスクタイプ。Promise.then、MutationObserverなど
Forced Synchronous Layout強制同期レイアウトJavaScriptでレイアウトプロパティの読み取りと書き込みを交互に行うことで、ブラウザが即座にレイアウト計算を強制されるパフォーマンス問題
Layout Thrashingレイアウトスラッシング頻繁な強制同期レイアウトによってパフォーマンスが急激に低下する現象
Virtual Scrolling仮想スクロールビューポート内の可視リスト項目のみをレンダリングする技術。大規模データリストのパフォーマンス最適化に使用
RAFrequestAnimationFrameブラウザが提供するAPI。次のリペイント前にアニメーション関連のJavaScriptコードを実行するために使用