16
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Frontend Performance - Part 18]. Core Web Vitals完全攻略:LCP・INP・CLSの実践的改善

16
Posted at

ChatGPT Image May 18, 2026, 02_22_26 PM.png

📝 注意
本記事はAIの補助を受けて編集しています。


📚 目次


0. はじめに:Lighthouse 100点でもユーザーが「遅い」と感じる理由

こんな経験はありませんか?

  • Lighthouseで100点を取得したのに、ユーザーから「クリックしても反応が遅い」と報告される
  • ローカル環境では非常に高速に動作するのに、遠隔地のユーザーだけが「スクロールがカクつく」と感じる
  • 原因の特定に多くの時間を費やした ことがある

上図はあくまで例です。実際の数値はアプリケーションやネットワーク状況に依存します。

コードやキャッシュ戦略を徹底的に改善してもユーザー体験が改善しない場合、課題は計測の方法にあります。Lighthouse(ラボ計測)は理想的な環境で測定するため、実ユーザーの体感と乖離することがあります。実際のユーザーは以下のような環境でアクセスします:

  • 3G/4Gモバイルネットワーク
  • ミドルレンジのスマートフォン
  • リソースを消費するブラウザ拡張機能

そこで、2024年3月以降、Googleは初回インタラクションのみを測定するFIDに代わり、INP(Interaction to Next Paint) を正式なCore Web Vitalsに採用しました。INPはセッション全体のすべてのインタラクションを測定します。

Part 18では、以下の問いに答えます:

「LCP、INP、CLSを体系的に改善し、Lighthouseだけでなく実ユーザーデータに基づいて最適化するにはどうすればよいか?」


1. Core Web Vitalsとは?ユーザー体験の三本柱

Core Web Vitalsは、Googleが定義する実ユーザー体験の指標で、2021年からSEOランキングに直接影響します。

Google公式の「良好(Good)」のしきい値(2026年現在):

指標 良好のしきい値 測定内容
LCP < 2.5秒 ビューポート内の最大コンテンツ(ヒーロー画像、見出しなど)の表示時間
INP < 200ms セッション中のすべてのインタラクション(クリック、タップ、キーボード)の遅延
CLS < 0.1 予期しないレイアウトシフトの程度

補足: 多くの競争の激しいプロダクト(EC、SaaS、大規模メディア)では、公式しきい値よりも厳しい社内目標を設定することがあります(例: LCP < 2.0秒, INP < 150ms, CLS < 0.05)。ただし、これらは公式のしきい値ではありません。


2. LCP(Largest Contentful Paint) – メインコンテンツの表示速度を改善する

目標: LCP < 2.5秒、理想的には4G回線で < 2.0秒。

LCPは通常、ヒーロー画像、大きな見出し、または背景動画のポスターです。

2.1. LCP要素を特定する

改善の前に、どの要素がLCPになっているかを特定します。DevToolsのPerformanceパネル(Web Vitalsトラック)やLighthouseで確認できます。LCP要素はモバイルとデスクトップで異なる場合があります。

2.2. LCP内訳分析 – 4つのフェーズ

ChromeはLCPを4つのフェーズに分解します。各フェーズのボトルネックを特定することで、適切な対策を打てます。

フェーズ 意味 よくある原因
TTFB サーバー処理時間 バックエンド遅延、キャッシュミス、CDN未使用
リソース読み込み遅延 LCPリソースの読み込み開始までの待ち時間 preload不足、読み込み順序の問題
リソース読み込み時間 画像・フォントなどの転送時間 画像が重い、WebP/AVIF未使用
要素レンダリング遅延 読み込み完了から実際に描画されるまでの時間 JavaScriptブロッキング、ハイドレーション、CSS未準備

例: リソース読み込み時間が長い → 画像圧縮; 要素レンダリング遅延が長い → クリティカルCSSの確認、不要なJSのdefer。

2.3. TTFB(Time to First Byte)の改善

問題: サーバー応答に800msかかる場合、LCP 2.5秒は達成不可能。国内トラフィックではTTFB < 200msを目標に。

改善策:

  • CDNエッジでのフルページキャッシュ
  • データベースクエリの最適化(インデックス、クエリキャッシュ)
  • エッジコンピューティング(Cloudflare Workers, Lambda@Edge)による動的コンテンツのキャッシュ

2.4. LCPリソースのプリロード

LCP要素が画像やフォントの場合、ブラウザに早期読み込みを指示します。

<link rel="preload" as="image" href="/hero.webp" fetchpriority="high" />
<link rel="preload" as="font" href="/fonts/inter.woff2" crossorigin />

Next.jsの場合: 手動プリロードの代わりに、next/imagepriority プロパティを使うと自動的にpreloadされます。

<Image src="/hero.webp" alt="Hero" width={1200} height={600} priority />

注意: 本当に必要なリソースにのみプリロードを使用してください。過剰なプリロードは帯域を消費し、LCPを悪化させます。

2.5. レンダリングブロッキングリソースを避ける

問題: 同期的なCSS/JSリクエストがLCPの表示を遅らせます。

改善策:

  • クリティカルCSSのインライン化
  • 不要なJSのdefer/async化
  • 未使用CSS/JSの削除(Coverageパネルで確認)

3. INP(Interaction to Next Paint) – インタラクションの滑らかさを改善する

目標: INP < 200ms、理想的には ≤ 150ms。

INPはセッション中のすべてのインタラクション(クリック、タップ、キーボード入力)の遅延を測定します。FID(初回のみ)とは異なり、INPは一度でも遅いインタラクションがあればペナルティを受けます。最も難しい指標の一つです。

INPは3つのフェーズに分かれます:

  1. 入力遅延: メインスレッドが空くまでの待ち時間
  2. 処理時間: イベントハンドラの実行時間
  3. 表示遅延: 応答をレンダリングする時間

3.1. ロングタスクの分割(Task Chunking)

ロングタスク(50ms以上)はINP悪化の主要因です。タスクを分割します。

基本(setTimeout を使用):

function processChunk(items: string[], chunkSize = 100) {
  let index = 0;
  function run() {
    const end = Math.min(index + chunkSize, items.length);
    for (let i = index; i < end; i++) {
      processItem(items[i]);
    }
    index = end;
    if (index < items.length) {
      setTimeout(run, 0);
    }
  }
  run();
}

よりモダンな方法(Scheduler API、フォールバック付き):

async function processWithYield(items: string[]) {
  for (let i = 0; i < items.length; i++) {
    processItem(items[i]);
    if (i % 50 === 0) {
      if ('scheduler' in window && 'yield' in scheduler) {
        await scheduler.yield(); // Chrome/Edge
      } else {
        await new Promise(r => setTimeout(r, 0)); // Safariフォールバック
      }
    }
  }
}

注意: scheduler.yield() はChrome/Edgeでサポートされていますが、Safariでは未対応です。必ずフォールバックを用意してください。

3.2. レイアウトスラッシングを避ける

レイアウトスラッシングは、JavaScriptがスタイル変更の直後にレイアウト情報を読み取ることで発生します。

NG例:

// ❌ 問題のある例 – レイアウトスラッシング
function badResize(elements: HTMLElement[]) {
  elements.forEach(el => {
    const h = el.offsetHeight;      // レイアウト読み取り
    el.style.height = h + 10 + 'px'; // レイアウト書き込み → 強制リフロー
    const w = el.offsetWidth;       // 再読み取り → 再度リフロー
    el.style.width = w + 10 + 'px';
  });
}

改善例:

// ✅ 改善例 – 読み取りと書き込みのバッチ処理
function goodResize(elements: HTMLElement[]) {
  const sizes = elements.map(el => ({
    h: el.offsetHeight,
    w: el.offsetWidth,
  }));
  elements.forEach((el, i) => {
    el.style.height = sizes[i].h + 10 + 'px';
    el.style.width = sizes[i].w + 10 + 'px';
  });
}

レイアウトスラッシングはイベントハンドラを遅くし、INPの処理時間を増加させます。

3.3. Web Workerの使用(ただし慎重に)

重い計算(画像処理、暗号化、大規模配列のソートなど)はWeb Workerに移します。

const worker = new Worker(new URL('./heavy-task.js', import.meta.url));
worker.postMessage(largeArray);
worker.onmessage = (e) => {
  setResult(e.data);
};

注意: Web Workerが常に高速とは限りません。シリアライズ/デシリアライズとメッセージ送信のオーバーヘッドが大きい場合があります。実際に10〜20ms以上かかる重いタスクにのみ使用してください。

3.4. Reactでの useTransition / useDeferredValue の活用

React 18以降では、useTransition を使って重要度の低い状態更新の優先度を下げ、メインスレッドをインタラクション応答に集中させます。

const [isPending, startTransition] = useTransition();
startTransition(() => {
  setFilter(value); // 緊急でない更新
});

3.5. イベントハンドラの最適化と不要なサードパーティスクリプトの削除

  • 頻繁に発生するハンドラはthrottle/debounce
  • 不要なスクリプトは削除し、残すものは defer または async
  • 非クリティカルなタスクには requestIdleCallback を使用(ただし、実行タイミングが保証されないため、UX上重要な処理には使わないこと)

4. CLS(Cumulative Layout Shift) – レイアウトの安定性を改善する

目標: CLS < 0.1、理想的には < 0.05。

4.1. 画像・iframe・動画に明示的なサイズを指定する

Web全体のCLS問題の約40%は、サイズ未指定の画像が原因です。

NG例:

<img src="hero.jpg" alt="Hero" />

改善例:

<img src="hero.jpg" alt="Hero" width="1200" height="600" />

またはCSSの aspect-ratio を使用:

.image-container {
  aspect-ratio: 1200 / 600;
}

4.2. font-display: swap と重要なフォントのプリロード

フォント読み込み時のサイズ変更を防ぐため、swap を使用します。

@font-face {
  font-family: 'Inter';
  font-display: swap;
  src: url('/fonts/inter.woff2') format('woff2');
}

4.3. 動的コンテンツ用のプレースホルダー(placeholder)を用意する

遅延読み込みされるコンテンツ(コメント、バナー、広告)には、固定の高さを確保したプレースホルダーを用意します。

特にサードパーティ製の要素(広告、Cookie同意バナー、レコメンドウィジェット、A/Bテスト注入、チャットポップアップ)はCLSの原因になりやすいため、事前にスペースを確保してください。

4.4. アニメーションには transform を使用する

レイアウトプロパティ(width, height, margin, padding)を変更するアニメーションはCLSを引き起こします。transform を使用してリフローを防ぎます。

/* ❌ CLSを引き起こす */
@keyframes slide {
  from { left: 0; }
  to { left: 100px; }
}

/* ✅ CLSを引き起こさない */
@keyframes slide {
  from { transform: translateX(0); }
  to { transform: translateX(100px); }
}

4.5. ビューポート上部への予期しないコンテンツ挿入を避ける

既存コンテンツの上に新しいDOMを挿入するとシフトが発生します。必ずプレースホルダーを使用するか、挿入場所を慎重に設計してください。

4.6. content-visibility による描画最適化

画面外の長いコンテンツには content-visibility: auto; を使用すると、ブラウザが必要になるまでレンダリングをスキップできます。

.long-list {
  content-visibility: auto;
  contain-intrinsic-size: 0 500px;
}

注意: content-visibility はアナリティクスのビュー計測、アクセシビリティ、スクロール動作に影響を与える可能性があります。本番適用前に十分なテストを行ってください。


5. ラボ計測とフィールド計測 – DevToolsから実ユーザーへ

Chrome DevToolsはラボ環境(ローカルマシン、高速ネットワーク)でのデバッグに適しています。しかし本番環境はまったく異なります(ユーザーのネットワーク、多様なデバイス、実際のキャッシュ)。

重要な違い:

  • ラボのCLSは読み込み中にしか発生しませんが、フィールドのCLSは後から読み込まれるコンテンツ(コメント、広告、チャット)でも発生します。
  • ラボでINPを再現するのは困難です。フィールドINPはセッション全体を記録します。

そのため:

  • CrUXやSearch ConsoleのCore Web Vitalsレポートで実データを確認する。
  • RUM(Datadog, Sentry, New Relic)を使ってリージョンやデバイス別にフィルタリングする。

web-vitalsライブラリでの計測例:

import { onLCP, onINP, onCLS } from 'web-vitals';

onLCP(console.log);
onINP(console.log);
onCLS(console.log);

より高度な例(PerformanceObserver):

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.entryType === 'largest-contentful-paint') {
      console.log('LCP:', entry.startTime);
    }
  }
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });

6. React/Next.js – CWV改善における特有の注意点

6.1. SPAでのルートベース・遅延読み込み

SPAでは多くのコンポーネントが初期ロード時に読み込まれ、LCPが遅くなることがあります。React.lazy + Suspenseを使ってルート単位でコード分割しましょう。

6.2. Next.js App RouterとReact Server Components

Next.js App Router + React Server Componentsは、一部のアーキテクチャでクライアントへのJavaScript転送量を削減し、ハイドレーションコストを下げることでINP/LCPを改善できる可能性があります。ただし効果はアプリケーションの構造に大きく依存するため、万能ではありません。

ハイドレーションに関する注意:

  • Next.js App RouterでClient Componentが多すぎると、ハイドレーションコストが増加しLCP/INPに悪影響を与える可能性があります。
  • 可能な限り、クライアント側のインタラクションが不要な部分はServer Componentsを優先しましょう。

6.3. 賢いプリフェッチとプリロード

Reactでは、ユーザーがホバーした時だけルートをプリフェッチするなどの工夫をしましょう。LCP要素のプリロードも同様に重要です。

6.4. バンドル分析とJavaScript削減

重いライブラリ(チャート、UIフレームワーク)はINPを悪化させます。バンドルアナライザを使って分析し、ルートベースの遅延読み込みを適用しましょう。


7. 補足:BFCache – 戻る/進む操作の体験向上

Core Web Vitals以外にも、back/forward cache(bfcache) はユーザー体験に大きく影響します。bfcacheはユーザーが離脱したページをメモリに保持し、戻る/進む操作で即座に表示します。

bfcacheの妨げになる要因:

  • unload イベントの使用(または非互換なAPI)
  • WebSocketや開きっぱなしの接続
  • 一部のブラウザでは Cache-Control: no-store ヘッダが影響する場合があります。

bfcacheのテスト方法: DevTools → Application → Back/forward cache → Test。非互換があればコードを修正し、ユーザーのナビゲーション体験を向上させましょう。


8. アーキテクト向けチェックリスト

LCP

  • LCP要素(ヒーロー画像、見出し)を特定済み
  • TTFB < 200ms(キャッシュ/CDN使用時)
  • LCP要素を fetchpriority="high" でプリロード(Next.jsでは priority プロパティ)
  • 画像を圧縮しWebP/AVIFを使用
  • LCP前にレンダリングブロッキングCSS/JSがない
  • LCP内訳分析でボトルネックを特定

INP

  • ロングタスク(>50ms)を分割またはWeb Worker化(オーバーヘッド考慮)
  • レイアウトスラッシングを回避(読み取りと書き込みのバッチ処理)
  • イベントハンドラを軽量に、リフローを最小化
  • 不要なサードパーティスクリプトを削除またはdefer
  • Reactでは useTransition / useDeferredValue を活用
  • scheduler.yield() を使用する場合はフォールバック(setTimeout)を用意

CLS

  • 画像・動画・iframeに明示的なサイズを指定(width/height または aspect-ratio)
  • すべてのWebフォントに font-display: swap
  • 後から読み込まれるコンテンツ(広告、コメント、Cookie同意バナー)にプレースホルダーを用意
  • アニメーションには transform を使用
  • content-visibility: auto を適用(分析、アクセシビリティ、スクロール動作を事前テスト)

監視

  • CrUX / Search ConsoleまたはRUMでCore Web Vitalsを確認
  • web-vitals ライブラリを組み込みフィールドデータを収集
  • Lighthouseだけで判断しない
  • bfcache互換性をテスト

9. まとめと次回予告

指標 意味 主要な改善テクニック
LCP メインコンテンツ表示速度 LCP要素のプリロード、TTFB最適化、レンダリングブロッキング回避、画像圧縮
INP インタラクション応答性 ロングタスク分割、レイアウトスラッシング回避、Web Worker(適宜)、useTransition
CLS レイアウト安定性 サイズ明示、font-display: swap、プレースホルダー、transformアニメーション

👉 次回予告(Part 19):
[Frontend Performance - Part 19] ユーザーが感じる遅さを可視化するWebパフォーマンス計測(RUM)


16
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?