クリティカルレンダリングパス最適化|FCP・LCPを改善する実践ガイド
クリティカルレンダリングパス(CRP)最適化の本質は、最初の描画を止めているリソースを減らし、LCP要素を最優先で届けることだ。外部CSSのインライン化、defer/asyncによるJSの非ブロッキング化、LCP画像へのpreload——この3つだけでもFCPは数百ミリ秒、LCPは1秒近く改善するケースは珍しくない。
クリティカルレンダリングパス(CRP)とは
クリティカルレンダリングパス(Critical Rendering Path, CRP)とは、ブラウザがサーバーからHTMLを受け取ってから、画面に最初のコンテンツ(First Contentful Paint)を描画するまでに必ず通過する一連の処理とリソースの経路のことだ。
CDNを導入してもTTFBが速いのに「白い画面が長い」——そんな体感の差は、ほぼCRP上のボトルネックが原因になる。ネットワーク往復(RTT)の問題ではなく、ブラウザ内部のパース・スタイル計算・スクリプト実行の待ちが支配している状態だ。
CRPを短くするには次の3点を同時に意識する。
- クリティカルリソースの数を減らす — 最初の描画に不要なCSS/JS/フォントを後回しにする
- クリティカルパスの長さを短くする — リソース数 × RTT を減らす(HTTP/2/3、preconnect、preload)
- クリティカルバイト数を減らす — 圧縮、未使用CSS除去、画像最適化
Core Web VitalsのFCP(First Contentful Paint)とLCP(Largest Contentful Paint)は、CRPの長さをユーザー体感に直結させた指標だ。FCPは最初のテキスト・画像が表示される時刻、LCPはビューポート内で最も大きなコンテンツが描画される時刻を示す。目安はLCP 2.5秒以内。
DOM・CSSOM・レンダーツリーの構築フロー
ブラウザの描画パイプラインは、大きく次の段階で進む。CRP最適化の前提として、この流れを頭に入れておく。
- DOM(Document Object Model) — HTMLトークンをパースし、ノードツリーを構築する
- CSSOM(CSS Object Model) — CSSルールをパースし、スタイル情報のツリーを構築する
- レンダーツリー(Render Tree) — DOMとCSSOMを合成し、実際に画面に描画する要素だけを抽出する
- レイアウト(Layout / Reflow) — 各要素の位置・サイズを計算する
- ペイント(Paint) — ピクセルを描画し、CompositeでGPUレイヤーに合成する
| モデル | 役割と特徴 |
|---|---|
| DOM | HTMLの構造ツリー。display:none の要素も含む |
| CSSOM | 全スタイルルールを解釈したツリー。レンダーツリー構築の前提 |
| レンダーツリー | DOMとCSSOMの合成。非表示要素は除外される |
| レイアウト | 各要素の位置・サイズを計算(Reflow) |
| ペイント | ピクセルを画面に描画。CompositeでGPU合成 |
重要なのは、CSSOMが完成するまでレンダーツリーは作れないという点だ。外部CSSファイルが<head>にある限り、ブラウザはCSSOM構築を優先し、First Paintは後ろ倒しになる。同様に、defer/asyncのない<script>に遭遇するとHTMLパースが一時停止し、DOM構築自体が止まる。
レンダーブロッキングリソースの正体
レンダーブロッキングリソース(Render-blocking Resources)とは、ブラウザが最初の描画を行う前に、ダウンロードまたは実行を待たなければならないリソースのことだ。
典型的なブロッカーは次のとおり。
| リソース | ブロック対象 | 理由 |
|---|---|---|
<head>内の外部CSS | レンダーツリー構築・First Paint | CSSOM完成まで描画不可 |
defer/asyncなしの<script> | HTMLパース・DOM構築 | パーサーが停止し実行を待つ |
Webフォント(font-display: block) | テキスト描画(FOIT) | フォント到着まで文字が不可視 |
| 同期XHR(レガシー) | JS実行スレッド | 現在は非推奨だがレガシーコードに残存 |
Lighthouseの「Eliminate render-blocking resources」診断は、Above-the-fold(最初の画面)に不要なCSS/JSを検出する。Chrome DevToolsのCoverageパネル(Cmd+Shift+P → “Coverage”)を使うと、読み込んだCSS/JSのうち実際に使われた割合がわかり、削減候補を定量的に把握できる。
最適化前後のFCP改善イメージ
上記は同一ページで段階的に最適化した際のFCP(ミリ秒)の改善イメージだ。数値は環境依存だが、CSSインライン化 → JS defer → LCP preloadの順で効果が積み上がる典型パターンを示している。
HTML・CSS・JavaScriptが描画を止める具体例
理論だけでなく、実際のHTMLがどうパースを止めるかをコードで確認しよう。
悪い例:レンダーブロッキングが集中した<head>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>ブロッキングだらけのページ</title>
<!-- ① CSSOM完成までFirst Paint不可 -->
<link rel="stylesheet" href="/css/bootstrap.min.css"> <!-- 150KB -->
<link rel="stylesheet" href="/css/theme.css"> <!-- 80KB -->
<link rel="stylesheet" href="/css/components.css"> <!-- 45KB -->
<!-- ② HTMLパースがここで完全停止。ダウンロード→実行→再開 -->
<script src="/js/jquery.min.js"></script>
<!-- ③ さらに停止。jQuery依存のため順序も固定 -->
<script src="/js/app.js"></script>
</head>
<body>
<h1>ようこそ</h1>
<img src="/images/hero.webp" alt="ヒーロー" width="1200" height="630">
</body>
</html>
この構成では、ブラウザは次の順序で待つ。
- HTML受信開始 →
<link>CSS 3ファイルを並列ダウンロード(CSSOM構築待ち) - CSSOM完成まで白い画面が続く
<script src="jquery">でHTMLパース停止 → ダウンロード → 実行 → 再開<script src="app.js">で再び停止 → ダウンロード → 実行 → 再開- ようやく
<body>のパース → レイアウト → ペイント
275KBのCSSのうちAbove-the-foldに必要なのは数KB程度、というケースも多い。全CSSを<head>で同期読み込みするのが最大のボトルネックになりやすい。
CSSが描画を止めるメカニズム
<!-- レンダーブロッキング:CSSOM完成まで待つ -->
<link rel="stylesheet" href="/css/all.css">
<!-- 非ブロッキング化の定番パターン -->
<link rel="stylesheet" href="/css/all.css" media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="/css/all.css"></noscript>
media="print"を付けると、ブラウザは「印刷用CSS」と判断しFirst Paintをブロックしない。onloadでmedia='all'に切り替えることで、描画後にフルスタイルが適用される。FOUC(スタイル未適用コンテンツの一瞬の露出)は起きうるが、FCP改善とのトレードオフとして許容されることが多い。
JavaScriptがHTMLパースを止める例
<!-- パーサーブロッキング:遭遇した瞬間に停止 -->
<script src="/js/heavy-analytics.js"></script>
<!-- document.write はパース中にDOMを書き換えるため特に危険 -->
<script>
document.write('<div id="banner">キャンペーン</div>');
</script>
<!-- インラインでも外部ファイルと同様にブロックする -->
<script>
const config = fetchSync('/api/config'); // 同期処理は避ける
</script>
<script>はデフォルトでパーサーブロッキングだ。<body>末尾に置いても、パースが<script>位置まで進むまでは実行されない。DOMContentLoadedの発火も遅れる。
defer と async:scriptタグの正しい使い分け
HTML Living Standard では、<script>に3つの読み込みモードがある。
| 属性 | ダウンロード / パース / 実行タイミング |
|---|---|
| (なし) | ダウンロード中パース停止 → 実行 → 再開。最も遅い |
| defer | ダウンロードは並行、実行はDOM構築完了後。記述順に実行 |
| async | ダウンロードは並行、実行は完了次第。順序保証なし |
| type=module | defer相当。ES Moduleはデフォルトでdefer挙動 |
改善後の<head>構成例
<head>
<meta charset="UTF-8">
<title>最適化後のページ</title>
<!-- LCP画像を最優先で先読み(後述) -->
<link rel="preload" as="image" href="/images/hero.webp"
imagesrcset="/images/hero-800.webp 800w, /images/hero-1200.webp 1200w"
imagesizes="100vw">
<!-- クリティカルCSSはインライン(次セクション) -->
<style>
/* Above-the-fold に必要な最小CSS */
body { margin: 0; font-family: system-ui, sans-serif; }
.hero { min-height: 60vh; background: #1a1a2e; color: #fff; }
h1 { font-size: clamp(2rem, 5vw, 3.5rem); }
</style>
<!-- 残りCSSは非ブロッキング -->
<link rel="stylesheet" href="/css/all.css" media="print" onload="this.media='all'">
<!-- アプリ本体:DOM完成後に順序どおり実行 -->
<script src="/js/app.js" defer></script>
<!-- 独立した計測タグ:完了次第実行でOK -->
<script src="/js/analytics.js" async></script>
</head>
deferはDOMContentLoadedの直前、記述順に実行される。jQuery → アプリ本体のように依存関係がある場合はdeferを選ぶ。asyncはダウンロード完了次第すぐ実行されるため、他スクリプトへの依存がないタグ向きだ。
<script type="module">はデフォルトでdefer相当の挙動になる。バンドラー(Vite、Webpack)の出力を<script type="module">にすれば、パーサーブロッキングを回避しやすい。
クリティカルCSSのインライン化テクニック
クリティカルCSS(Critical CSS)とは、Above-the-fold(スクロールなしで見える範囲)を正しく表示するために必要な最小限のCSSのことだ。これをタグでHTMLに直接埋め込むことで、外部CSSファイルのダウンロードを待たずにFirst Paintできる。
手動抽出の手順
- Chrome DevTools → Rendering → 「Emulate CSS media: screen」で通常表示
- Above-the-foldの要素(ヘッダー、ヒーロー、
<h1>)を特定 - DevTools Coverageで
/css/all.cssを開き、使用行だけをコピー <head>内に<style>として貼り付け- 残りのCSSは非同期または
<link rel="preload" as="style">で後から読み込む
ビルドツールでの自動化
// vite.config.js — vite-plugin-critical 等の利用例(概念)
import critical from 'vite-plugin-critical';
export default {
plugins: [
critical({
// Above-the-fold CSSを<style>にインライン化
inline: true,
// 残りCSSは別ファイルとして非同期読み込み
extract: true,
dimensions: [{ width: 375, height: 667 }, { width: 1920, height: 1080 }],
}),
],
};
Critters(Google製)、Penthouse、critical npmパッケージなども同様の役割を担う。SSR/SSG(Astro、Next.js)ではHTML生成時にクリティカルCSSを注入するプラグインが一般的だ。
インライン化の注意点
- HTMLサイズが膨らむ — 10KB超のインラインCSSはTTFB悪化の原因になる。5KB前後に抑えるのが目安
- キャッシュ効率 — インラインCSSはHTMLと一緒にキャッシュされる。外部CSSの長期キャッシュとトレードオフ
- FOUC対策 — 非同期CSS到着前後でレイアウトが変わる場合は、スケルトンUIや
min-heightで領域を確保する
preload と preconnect による優先度制御
<link rel="preload">は、ブラウザに「このリソースを今すぐ使うから高優先度で取得せよ」と指示するディレクティブだ。LCP要素の画像やWebフォントに効果的だ。
LCP画像へのpreload
<head>
<!-- ヒーロー画像(LCP候補)を最優先先読み -->
<link rel="preload" as="image"
href="/images/hero-1200.webp"
imagesrcset="/images/hero-800.webp 800w, /images/hero-1200.webp 1200w"
imagesizes="(max-width: 768px) 100vw, 1200px"
fetchpriority="high">
<!-- Webフォント -->
<link rel="preload" as="font" href="/fonts/noto-sans.woff2"
type="font/woff2" crossorigin>
<!-- 重要CSS(インライン化しない場合の代替) -->
<link rel="preload" as="style" href="/css/critical.css"
onload="this.onload=null;this.rel='stylesheet'">
</head>
<body>
<img src="/images/hero-1200.webp"
srcset="/images/hero-800.webp 800w, /images/hero-1200.webp 1200w"
sizes="(max-width: 768px) 100vw, 1200px"
alt="サービス紹介"
width="1200" height="630"
fetchpriority="high">
</body>
as属性は必須だ。値を間違えるとブラウザが優先度を正しく設定できず、preloadの効果が半減する。フォントpreloadにはcrossoriginが必要(CORSモードを一致させるため)。
| ディレクティブ | 用途と優先度 |
|---|---|
| preload | 現在のページですぐ使うリソース。Highest優先度 |
| prefetch | 次のナビゲーションで使う可能性があるリソース。Low優先度 |
| preconnect | CDN・APIオリジンへのDNS+TCP+TLS接続を事前確立 |
| dns-prefetch | DNS解決のみ事前実行。preconnectの軽量版 |
<!-- Google FontsやCDNへの接続を事前確立 -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
preloadは強力だが帯域の奪い合いを起こす。Lighthouseが「Preload key requests」を指摘したら、LCP要素とクリティカルフォントに絞り、それ以外のpreloadは削除する。
Lighthouseウォーターフォールの読み方
Lighthouse単体のスコアより、DevTools Networkパネルのウォーターフォールを読む力の方がCRP改善では重要だ。
ウォーターフォールの見方
- Chrome DevTools → Network → 「Disable cache」にチェック → ページリロード
- ウォーターフォールの横軸は時間(ms)。縦棒が長いほど待ち時間が大きい
- 色の意味:濃い線=待ち時間(Queueing/Stalled)、緑=ダウンロード、青=DOMContentLoaded、赤=Load
典型的な問題パターン:
| ウォーターフォールの見え方 | 意味 | 対策 |
|---|---|---|
| HTML直後にCSSが長い棒で並ぶ | CSSOM待ちでFirst Paintが遅延 | クリティカルCSSインライン化 |
| JSの棒がHTMLパース中に割り込む | パーサーブロッキング | defer/async化 |
| LCP画像の開始がCSS/JS完了後 | 画像優先度が低い | preload + fetchpriority=“high” |
| 多数リソースが1本の接続で順番待ち | HTTP/1.1的なボトルネック | HTTP/2/3、preconnect |
| フォントの棒がLCPテキストより後 | FOIT/FOUT | font-display: swap + preload |
Lighthouseタイムラインの読み方
LighthouseレポートのView TreemapとDiagnosticsセクションも併用する。
- First Contentful Paint — 最初のテキスト/画像が描画された時点。CRP最適化の直接効果が出る
- Largest Contentful Paint — 最大コンテンツ(多くはヒーロー画像か
<h1>)の描画完了 - Total Blocking Time (TBT) — JS実行によるメインスレッドブロック合計。CRP後のインタラクティブ性に影響
- Speed Index — 視覚的完成速度の総合指標
Lighthouseはシミュレート回線(Slow 4G等)でのLab計測だ。本番のField Data(Search Console → Core Web Vitals、CrUX)は実ユーザーの回線・端末を反映する。Lab改善がField改善に直結するとは限らないため、両方を見る。
LCP要素の特定と最適化
LCP改善の第一歩は、自分のページで何がLCP要素になっているかを特定することだ。
DevToolsでの特定手順
Chrome DevTools → Lighthouse → Performance カテゴリで分析を実行する
レポートの「Largest Contentful Paint element」セクションでDOMノードを確認する
Performance Insights パネル → 「LCP by phase」で TTFB / Load delay / Load time / Render delay の内訳を見る
Elements パネルで該当ノードを右クリック → 「Scroll into view」で画面上の位置を確認する
LCPが画像なら preload + 適切な sizes/srcset、テキストなら font-display + フォントpreloadを適用する
LCP要素タイプ別の対策
| LCPタイプ | 典型例 | 主な対策 |
|---|---|---|
| 画像 | ヒーロー<img>、CSS背景画像 | preload、WebP/AVIF、srcset/sizes、CDN |
| テキストブロック | <h1>、リード文 | フォントpreload、font-display: swap、SSR |
| 動画ポスター | <video poster="..."> | ポスター画像のpreload、動画自体は遅延読み込み |
<!-- LCPが<h1>テキストの場合 -->
<head>
<link rel="preload" as="font" href="/fonts/noto-sans-jp.woff2"
type="font/woff2" crossorigin>
<style>
@font-face {
font-family: 'Noto Sans JP';
src: url('/fonts/noto-sans-jp.woff2') format('woff2');
font-display: swap; /* FOIT回避 */
}
h1 { font-family: 'Noto Sans JP', sans-serif; }
</style>
</head>
LCPの内訳でLoad delayが大きい場合、preload不足か優先度の問題だ。Load timeが大きい場合、画像サイズ・フォーマット・CDN距離を見直す。Render delayが大きい場合、レンダーブロッキングCSS/JSが原因のことが多い。
実践チェックリスト:CRP最適化の手順
Lighthouse Performance分析を実行し、FCP・LCP・TBTの現状値を記録する
NetworkウォーターフォールでレンダーブロッキングCSS/JSを特定する
Coverageパネルで未使用CSS/JSの割合を確認し、分割・Tree Shakingを検討する
Above-the-fold CSSを<style>にインライン化し、残りCSSは非同期読み込みに切り替える
scriptタグにdefer/asyncを付け、インラインスクリプトは最小限にする
LCP要素を特定し、preload・fetchpriority=high・画像最適化を適用する
preconnectでCDN・フォントオリジンへの接続を事前確立する
改善後にLighthouseとWeb Vitals(Field Data)で効果を検証・記録する
よくある落とし穴
- 全CSS/JSを1ファイルにバンドル — キャッシュ効率は上がるが、CRPは悪化する。Above-the-fold用に分割を検討
- preloadの乱用 — 10個以上のpreloadは逆効果。LCP + クリティカルフォントに限定
- サードパーティスクリプトの放置 — 広告・チャット・分析タグがCRPを支配しているケースが多い。遅延読み込みまたはタグマネージャーで制御
- 画像のwidth/height未指定 — CLS(Cumulative Layout Shift)悪化の原因。LCP画像には必ず寸法属性を付ける
関連記事
この記事は Webパフォーマンス テーマの一環です。あわせて読むと理解が深まる関連記事をまとめました。
| トピック | 記事 |
|---|---|
| HTTP/2とHTTP/3の違い | HTTP/2とHTTP/3の違い|QUICがWeb表示速度を変える理由 |
| Speculation Rules API 完全ガイド | Speculation Rules API 完全ガイド|prefetch・prerender・Chrome 144 prerender_until_script |
| fetchpriority属性でLCP画像を最適化する | fetchpriority属性でLCP画像を最適化する|2026年実践ガイド |
| Web Vitals INP改善ガイド | Web Vitals INP改善ガイド|scheduler.yield・Long Task・Input Delay対策(2026) |
| content-visibility: auto 完全ガイド | content-visibility: auto 完全ガイド|遅延レンダリングと長尺ページ最適化 2026 |
| Server-Timing API 完全ガイド | Server-Timing API 完全ガイド|Express ミドルウェアと Chrome DevTools 連携 |
| Brotli vs Gzip 圧縮比較 | Brotli vs Gzip 圧縮比較|nginx静的・動的設定とCDN実践ガイド |
| CSS/JSを圧縮(Minify)するメリット | CSS/JSを圧縮(Minify)するメリット|サイト高速化の基本 |
| Service Worker キャッシュ戦略ガイド | Service Worker キャッシュ戦略ガイド|PWA の cache-first・network-first・SWR・Workbox 設計 |
| View Transitions API 完全ガイド | View Transitions API 完全ガイド|SPA・MPA ページ遷移と Astro/React 統合 |
| WebP変換で画質が落ちる原因と最適な設定 | WebP変換で画質が落ちる原因と最適な設定 |
| 画像圧縮とWebP変換 | 画像圧縮とWebP変換|PageSpeed改善の基本 |
まとめ
クリティカルレンダリングパス最適化は、CDN設定やHTTP/3だけでは完結しない。DOM・CSSOM・レンダーツリーのどこで待っているかをDevToolsウォーターフォールで可視化し、レンダーブロッキングリソースを段階的に削ることが核心だ。
実務では次の3点から始めるのが最も費用対効果が高い。
- クリティカルCSSのインライン化 — 外部CSS待ちを排除しFCPを直接改善
defer/asyncによるJS非ブロッキング化 — HTMLパース停止を解消- LCP要素への
preload— 最大コンテンツの到着を前倒し
まずLighthouseの診断結果を1枚保存し、ウォーターフォールで最大のブロッカーを1つ潰す——このサイクルを繰り返せば、FCP・LCPは着実に改善する。