#はじめに
「ページの表示が遅い」——その原因を特定するには、ブラウザがHTMLを受け取ってから画面に描画するまでの流れを理解する必要があります。
クリティカルレンダリングパス(Critical Rendering Path)は、ブラウザがHTMLを最初のピクセルとして画面に描画するまでの一連のステップです。このパスを最適化することで、ページの表示速度を大幅に改善できます。
この記事を読むと、以下のことができるようになります:
- ブラウザのレンダリングプロセスを理解できる
- レンダリングブロックの原因を特定できる
- 適切な最適化手法を選択できる
#レンダリングの流れ
#5つのステップ
[HTML] → [DOM] → [CSSOM] → [Render Tree] → [Layout] → [Paint]
- DOM構築: HTMLをパースしてDOMツリーを構築
- CSSOM構築: CSSをパースしてCSSOMツリーを構築
- Render Tree: DOMとCSSOMを組み合わせてRender Treeを構築
- Layout: 各要素の位置とサイズを計算
- Paint: 実際にピクセルを画面に描画
#詳細な流れ
[HTML受信]
↓
[HTMLパース]
↓
┌─────────┴─────────┐
↓ ↓
[DOM構築] [CSS取得]
│ ↓
│ [CSSパース]
│ ↓
│ [CSSOM構築]
│ │
└─────────┬─────────┘
↓
[Render Tree]
↓
[Layout]
↓
[Paint]
↓
[画面表示]
#DOM構築
#HTMLのパース
ブラウザはHTMLを上から順に読み込み、DOMツリーを構築します。
<!DOCTYPE html>
<html>
<head>
<title>サンプル</title>
</head>
<body>
<h1>見出し</h1>
<p>本文</p>
</body>
</html>
Document
└── html
├── head
│ └── title
│ └── "サンプル"
└── body
├── h1
│ └── "見出し"
└── p
└── "本文"
#インクリメンタルパース
ブラウザはHTMLを完全にダウンロードする前からパースを開始します。これにより、ページの一部を早く表示できます。
#CSSOM構築
#CSSのパース
CSSもパースされてツリー構造になります。
body {
font-size: 16px;
}
h1 {
color: blue;
}
p {
margin: 10px;
}
CSSOM
└── body (font-size: 16px)
├── h1 (color: blue, font-size: 16px)
└── p (margin: 10px, font-size: 16px)
CSSOMは継承を含むため、子要素は親のスタイルを継承します。
#CSSはレンダリングブロック
CSSが完全に読み込まれるまで、ブラウザはRender Treeを構築できません。
<head>
<!-- このCSSが読み込まれるまでレンダリングがブロック -->
<link rel="stylesheet" href="styles.css">
</head>
これは、スタイルが適用されていないコンテンツが一瞬表示される「FOUC(Flash of Unstyled Content)」を防ぐためです。
#JavaScriptのブロッキング
#パーサーブロッキング
<script>タグに到達すると、HTMLのパースが停止します。
<body>
<h1>見出し</h1>
<!-- ここでパースが停止 -->
<script src="app.js"></script>
<!-- スクリプト実行完了後に再開 -->
<p>本文</p>
</body>
これは、JavaScriptがDOMを操作する可能性があるためです。
#CSSとJavaScriptの順序
JavaScriptはCSSOMにもアクセスできるため、CSSの読み込みを待ってから実行されます。
[CSS読み込み] → [CSSOM構築] → [JS実行] → [DOMパース再開]
#ブロッキングの回避
#async属性
スクリプトを非同期で読み込み、読み込み完了後すぐに実行します。
<script src="analytics.js" async></script>
- HTMLパースをブロックしない
- 実行順序は保証されない
- DOMContentLoadedを待たない
用途: アナリティクス、広告など独立したスクリプト
#defer属性
スクリプトを非同期で読み込み、HTMLパース完了後に実行します。
<script src="app.js" defer></script>
- HTMLパースをブロックしない
- DOMContentLoaded前に実行
- 記述順に実行される
用途: DOMに依存するアプリケーションスクリプト
#asyncとdeferの違い
async:
[HTMLパース]──────────────────────>
[JS読み込み]──>[実行]
↑ 読み込み完了時に実行
defer:
[HTMLパース]──────────────────────>[実行]
[JS読み込み]────────────> ↑ パース完了後に実行
| 属性 | パースブロック | 実行タイミング | 実行順序 |
|---|---|---|---|
| なし | する | 即座 | 順番通り |
| async | しない | 読み込み完了時 | 不定 |
| defer | しない | パース完了後 | 順番通り |
#type=“module”
ES Modulesは自動的にdeferと同様の動作になります。
<script type="module" src="app.js"></script>
#クリティカルCSSのインライン化
ファーストビューに必要なCSSを<head>内に直接記述します。
<head>
<!-- クリティカルCSS(ファーストビュー用) -->
<style>
body { margin: 0; font-family: sans-serif; }
.header { background: #333; color: white; padding: 1rem; }
.hero { height: 50vh; display: flex; align-items: center; }
</style>
<!-- 残りのCSSは非同期で読み込み -->
<link rel="stylesheet" href="styles.css" media="print" onload="this.media='all'">
</head>
#メリット
- 外部CSSの読み込みを待たずにレンダリング開始
- LCPの改善
#デメリット
- HTMLサイズの増加
- CSSのキャッシュが効かない
- 管理の複雑化
#リソースの優先度制御
#fetchpriority属性
<!-- 重要なリソースの優先度を上げる -->
<link rel="stylesheet" href="critical.css" fetchpriority="high">
<img src="hero.jpg" fetchpriority="high" alt="...">
<!-- 重要でないリソースの優先度を下げる -->
<img src="footer-image.jpg" fetchpriority="low" alt="...">
#preload
クリティカルリソースを早期に取得します。
<head>
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="hero.jpg" as="image">
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
</head>
#最適化のベストプラクティス
#1. CSSを<head>の早い位置に
<head>
<meta charset="UTF-8">
<!-- CSSはできるだけ早く -->
<link rel="stylesheet" href="styles.css">
<!-- その他のmeta、scriptは後 -->
</head>
#2. JavaScriptを</body>直前またはdefer
<body>
<!-- コンテンツ -->
<!-- スクリプトは最後 -->
<script src="app.js"></script>
</body>
<!-- または -->
<head>
<script src="app.js" defer></script>
</head>
#3. 不要なリソースの削減
<!-- ❌ 使用しないCSSも読み込んでいる -->
<link rel="stylesheet" href="all-styles.css">
<!-- ✅ 必要なCSSのみ -->
<link rel="stylesheet" href="critical.css">
<link rel="stylesheet" href="page-specific.css">
#4. CSSの分割
<!-- メディアクエリでブロッキングを回避 -->
<link rel="stylesheet" href="base.css">
<link rel="stylesheet" href="print.css" media="print">
<link rel="stylesheet" href="large-screen.css" media="(min-width: 1200px)">
該当しないメディアクエリのCSSはレンダリングをブロックしません。
#DevToolsでの確認
#Performanceタブ
- DevToolsを開く
- Performanceタブを選択
- 記録を開始してページをリロード
- 以下を確認:
- Parse HTML(DOMパース時間)
- Parse Stylesheet(CSSパース時間)
- Evaluate Script(JS実行時間)
- Layout(レイアウト計算時間)
- Paint(描画時間)
#Networkタブ
- リソースの読み込み順序を確認
- レンダリングブロックリソースを特定
- Waterfallでボトルネックを発見
#Lighthouse
「Eliminate render-blocking resources」の警告で、ブロッキングリソースを特定できます。
#実践的な最適化例
#Before
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="framework.css">
<link rel="stylesheet" href="styles.css">
<script src="jquery.js"></script>
<script src="plugins.js"></script>
<script src="app.js"></script>
</head>
<body>
<img src="hero.jpg" alt="...">
<!-- コンテンツ -->
</body>
</html>
#After
<!DOCTYPE html>
<html>
<head>
<!-- クリティカルCSSをインライン化 -->
<style>
/* ファーストビューに必要なスタイルのみ */
</style>
<!-- 残りのCSSを非同期読み込み -->
<link rel="preload" href="styles.css" as="style" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>
<!-- LCP画像をpreload -->
<link rel="preload" href="hero.jpg" as="image">
<!-- スクリプトはdefer -->
<script src="app.js" defer></script>
</head>
<body>
<img src="hero.jpg" alt="..." fetchpriority="high">
<!-- コンテンツ -->
</body>
</html>
#まとめ
- クリティカルレンダリングパス: HTML→DOM→CSSOM→Render Tree→Layout→Paint
- CSSはレンダリングブロック: CSSOMが完成するまで描画できない
- JavaScriptはパーサーブロック:
async/deferで回避 - クリティカルCSSのインライン化: ファーストビューを高速化
- リソースの優先度制御:
fetchpriority、preloadを活用 - DevToolsで分析: Performanceタブでボトルネックを特定
#次のステップ
パフォーマンスと配信の基礎を学んだところで、次は発展トピックとしてリバースプロキシについて学びましょう。Webサーバーの前段に配置されるリバースプロキシの役割と設定方法を紹介します。