クリティカルレンダリングパス最適化|FCP・LCPを改善する実践ガイド

パフォーマンス CRP LCP FCP Web Vitals
結論

クリティカルレンダリングパス(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点を同時に意識する。

  1. クリティカルリソースの数を減らす — 最初の描画に不要なCSS/JS/フォントを後回しにする
  2. クリティカルパスの長さを短くする — リソース数 × RTT を減らす(HTTP/2/3、preconnect、preload)
  3. クリティカルバイト数を減らす — 圧縮、未使用CSS除去、画像最適化

Core Web VitalsのFCP(First Contentful Paint)とLCP(Largest Contentful Paint)は、CRPの長さをユーザー体感に直結させた指標だ。FCPは最初のテキスト・画像が表示される時刻、LCPはビューポート内で最も大きなコンテンツが描画される時刻を示す。目安はLCP 2.5秒以内。

DOM・CSSOM・レンダーツリーの構築フロー

ブラウザの描画パイプラインは、大きく次の段階で進む。CRP最適化の前提として、この流れを頭に入れておく。

  1. DOM(Document Object Model) — HTMLトークンをパースし、ノードツリーを構築する
  2. CSSOM(CSS Object Model) — CSSルールをパースし、スタイル情報のツリーを構築する
  3. レンダーツリー(Render Tree) — DOMとCSSOMを合成し、実際に画面に描画する要素だけを抽出する
  4. レイアウト(Layout / Reflow) — 各要素の位置・サイズを計算する
  5. ペイント(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 PaintCSSOM完成まで描画不可
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改善イメージ

3200
最適化前 FCP: 3200
最適化前 FCP
2100
CSS inline: 2100
CSS inline
1400
+ defer JS: 1400
+ defer JS
900
+ preload LCP: 900
+ preload LCP

上記は同一ページで段階的に最適化した際の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>

この構成では、ブラウザは次の順序で待つ。

  1. HTML受信開始 → <link> CSS 3ファイルを並列ダウンロード(CSSOM構築待ち)
  2. CSSOM完成まで白い画面が続く
  3. <script src="jquery"> でHTMLパース停止 → ダウンロード → 実行 → 再開
  4. <script src="app.js"> で再び停止 → ダウンロード → 実行 → 再開
  5. ようやく<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をブロックしないonloadmedia='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>

deferDOMContentLoadedの直前、記述順に実行される。jQuery → アプリ本体のように依存関係がある場合はdeferを選ぶ。asyncはダウンロード完了次第すぐ実行されるため、他スクリプトへの依存がないタグ向きだ。

moduleスクリプトを使うなら

<script type="module">はデフォルトでdefer相当の挙動になる。バンドラー(Vite、Webpack)の出力を<script type="module">にすれば、パーサーブロッキングを回避しやすい。

クリティカルCSSのインライン化テクニック

クリティカルCSS(Critical CSS)とは、Above-the-fold(スクロールなしで見える範囲)を正しく表示するために必要な最小限のCSSのことだ。これをタグでHTMLに直接埋め込むことで、外部CSSファイルのダウンロードを待たずにFirst Paintできる。

手動抽出の手順

  1. Chrome DevTools → Rendering → 「Emulate CSS media: screen」で通常表示
  2. Above-the-foldの要素(ヘッダー、ヒーロー、<h1>)を特定
  3. DevTools Coverage/css/all.cssを開き、使用行だけをコピー
  4. <head>内に<style>として貼り付け
  5. 残りの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改善では重要だ。

ウォーターフォールの見方

  1. Chrome DevTools → Network → 「Disable cache」にチェック → ページリロード
  2. ウォーターフォールの横軸は時間(ms)。縦棒が長いほど待ち時間が大きい
  3. 色の意味:濃い線=待ち時間(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/FOUTfont-display: swap + preload

Lighthouseタイムラインの読み方

LighthouseレポートのView TreemapDiagnosticsセクションも併用する。

  • First Contentful Paint — 最初のテキスト/画像が描画された時点。CRP最適化の直接効果が出る
  • Largest Contentful Paint — 最大コンテンツ(多くはヒーロー画像か<h1>)の描画完了
  • Total Blocking Time (TBT) — JS実行によるメインスレッドブロック合計。CRP後のインタラクティブ性に影響
  • Speed Index — 視覚的完成速度の総合指標
Lab計測の限界

Lighthouseはシミュレート回線(Slow 4G等)でのLab計測だ。本番のField Data(Search Console → Core Web Vitals、CrUX)は実ユーザーの回線・端末を反映する。Lab改善がField改善に直結するとは限らないため、両方を見る。

LCP要素の特定と最適化

LCP改善の第一歩は、自分のページで何がLCP要素になっているかを特定することだ。

DevToolsでの特定手順

1

Chrome DevTools → Lighthouse → Performance カテゴリで分析を実行する

2

レポートの「Largest Contentful Paint element」セクションでDOMノードを確認する

3

Performance Insights パネル → 「LCP by phase」で TTFB / Load delay / Load time / Render delay の内訳を見る

4

Elements パネルで該当ノードを右クリック → 「Scroll into view」で画面上の位置を確認する

5

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最適化の手順

1

Lighthouse Performance分析を実行し、FCP・LCP・TBTの現状値を記録する

2

NetworkウォーターフォールでレンダーブロッキングCSS/JSを特定する

3

Coverageパネルで未使用CSS/JSの割合を確認し、分割・Tree Shakingを検討する

4

Above-the-fold CSSを<style>にインライン化し、残りCSSは非同期読み込みに切り替える

5

scriptタグにdefer/asyncを付け、インラインスクリプトは最小限にする

6

LCP要素を特定し、preload・fetchpriority=high・画像最適化を適用する

7

preconnectでCDN・フォントオリジンへの接続を事前確立する

8

改善後に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点から始めるのが最も費用対効果が高い。

  1. クリティカルCSSのインライン化 — 外部CSS待ちを排除しFCPを直接改善
  2. defer/asyncによるJS非ブロッキング化 — HTMLパース停止を解消
  3. LCP要素へのpreload — 最大コンテンツの到着を前倒し

まずLighthouseの診断結果を1枚保存し、ウォーターフォールで最大のブロッカーを1つ潰す——このサイクルを繰り返せば、FCP・LCPは着実に改善する。