本文へスキップ
サイト診断

Core Web Vitals — LCP・INP・CLS の改善

Core Web Vitals は「速いかどうか」ではなく「速いと感じるかどうか」を測る指標です。そのため、読み込み時間を短くしても LCP が改善しないことがよくあります。3 指標が実際に何を測っているのかと、実ユーザーの数値を動かす改善策を説明します。

執筆: 公開:

Core Web Vitals とは何ですか?

Core Web Vitals は、Google が定めた 3 つのユーザー体験指標です。表示の速さ(LCP)、操作への反応の速さ(INP)、表示の安定性(CLS)を測ります。いずれも実際にサイトを訪れた人の環境で計測され、その 75 パーセンタイル値で合否が判定されます。

LCPLargest Contentful Paint最大要素が表示されるまで
良好
2.5 秒以下
不良
4.0 秒超
INPInteraction to Next Paint操作してから画面が更新されるまで
良好
200 ミリ秒以下
不良
500 ミリ秒超
CLSCumulative Layout Shift表示中のレイアウトのズレ量
良好
0.1 以下
不良
0.25 超

重要なのは 「75 パーセンタイル値で判定される」という点です。平均ではありません。 つまり訪問者の 75% が良好な体験をして初めて合格になります。 平均だと、高速な端末の利用者が多いだけで、低速回線のユーザーの体験が悪いことを覆い隠せてしまうためです。 ハイエンド端末の開発者が「うちは速い」と思い込む状況を防ぐ設計になっています。

LCP はページの読み込み時間とは何が違うのですか?

LCP が測るのは、画面内で最も大きい要素(多くはヒーロー画像や見出し)が表示されるまでの時間だけです。ページ全体の読み込み完了は関係ありません。そのため、読み込み自体は速くても、画像に lazy 属性が付いているだけで LCP は大きく悪化します。

load イベントや DOMContentLoaded は「ブラウザの仕事が終わったか」を測る指標でした。 しかし利用者にとっての体感は「見たかったものが見えたか」で決まります。両者は簡単に食い違います。

  • load は速いのに LCP が遅い — ヒーロー画像に loading="lazy" が付いている。 HTML の読み込みは一瞬で終わるが、肝心の画像が後回しにされる
  • load は遅いのに LCP は速い — 本文は即座に出るが、解析タグやチャットウィジェットが後ろで延々と読み込まれている。 体感上は何の問題も無い

LCP は 4 つの区間に分解できる

改善は「どの区間が長いか」を特定してから始めます。当てずっぽうで直しても数値は動きません。

LCP を構成する 4 区間と対策
区間何が起きているか効く対策
① TTFBサーバーが最初の 1 バイトを返すまでキャッシュ・CDN・DB クエリの改善
② 発見の遅れLCP 要素の存在にブラウザが気づくまでpreload・CSS 背景画像をやめる・JS 経由で読まない
③ 読み込み時間画像本体のダウンロードWebP / AVIF 化・圧縮・適切なサイズ配信
④ 描画の遅れ取得済みなのにまだ描けないレンダリングブロッキング CSS/JS の削減・font-display

実測では ② の「発見の遅れ」が犯人であることが非常に多いです。 画像自体は 50KB しかないのに、CSS の中に隠れているせいでブラウザが 1.5 秒間その存在に気づかない、というのが典型例です。

LCP を改善するには何をすればよいですか?

まず LCP 要素を特定し、それが「いつ発見され、いつ読み込まれ、いつ描画されたか」を分解します。実務で最も効くのは、ヒーロー画像から lazy を外して fetchpriority=high を付けることです。これだけで 1 秒以上改善する例は珍しくありません。

まず: lazy を外して優先度を上げる

ヒーロー画像の正しい書き方
<!-- ❌ 最悪パターン: ヒーロー画像に lazy。
     ブラウザは「後で読めばいい画像」と解釈し、他が終わるまで取りに行かない -->
<img src="/hero.webp" loading="lazy" alt="">

<!-- ✅ 最初の画面に映る画像は lazy を外し、優先度を上げる。
     width/height は CLS 対策にも必須(場所を先に予約させる) -->
<img src="/hero.webp" width="1200" height="630"
     fetchpriority="high" decoding="async" alt="製品の外観">

<!-- ✅ Next.js の Image なら priority を付けるだけ。
     fetchpriority=high と preload が自動で入る -->
import Image from "next/image";

<Image src="/hero.webp" width={1200} height={630} priority alt="製品の外観" />

loading="lazy" は「画面外の画像」にだけ付けるものです。「全画像に lazy を付ける」は、LCP を悪化させる最短の方法です。 パフォーマンス改善のつもりで一括適用され、そのまま放置されている例を頻繁に見かけます。

次に: 発見を早める

preload / preconnect
<!-- LCP 画像を CSS の background-image や JS から読んでいる場合、
     ブラウザのプリロードスキャナは HTML を見ただけでは発見できない。
     → 発見が数百 ms 遅れる。明示的に予告して埋め合わせる -->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high">

<!-- 外部ドメインから読むなら、DNS 解決と TLS handshake だけ先に済ませる -->
<link rel="preconnect" href="https://cdn.example.com" crossorigin>

<!-- ❌ やりすぎ注意: 何でも preload すると帯域を奪い合い、
     かえって LCP 要素の到着が遅れる。preload は 1〜2 個までが目安 -->

TTFB が遅いなら、画像をいくら削っても無駄

① の TTFB が 1.5 秒あるなら、LCP が 2.5 秒を切ることは原理的にありません。まずここを疑ってください。 本ツールは TTFB・圧縮・キャッシュ・リダイレクト連鎖を実測するので、サーバー側が足を引っ張っているかどうかは、JS の実行なしで判定できます

なお、本文を JavaScript で描画しているサイトは、この ④ の区間が構造的に長くなります。同時にAI クローラーからは本文が空に見えるという別の重大な問題も抱えます。詳しくは JavaScript なしで本文が読めないと、AI に引用されない を参照してください。

INP とは何ですか?

INP(Interaction to Next Paint)は、クリックやキー入力に対して画面が次に更新されるまでの時間を測る指標です。2024 年 3 月に FID を置き換えました。FID が最初の 1 回の待ち時間だけを見ていたのに対し、INP は滞在中のほぼ全操作を対象に、その最悪値に近い値を採用します。

FID(First Input Delay)との違いは決定的です。FID は最初の 1 回の、しかも「待たされた時間」だけを測っていたため、 ハンドラの処理がどれだけ重くても、待ち時間さえ短ければ good になりました。事実上ほぼ全サイトが合格する、意味の薄い指標でした。

INP は操作から次の描画までを丸ごと測り、滞在中のほぼ全操作の中から最悪値に近いものを採用します (操作回数が多いページでは、外れ値が 50 回につき 1 つ除外されます)。ごまかしが効きません。 FID で合格していたサイトが INP で不合格になる、というのはサイトが遅くなったのではなく、ようやく正しく測られるようになったということです。

改善の要点は「長いタスクを切る」こと

INP を悪化させない書き方
// ❌ クリックハンドラで全部やる。
//    描画が数百 ms 止まり、ユーザーには「固まった」と感じられる
button.addEventListener("click", () => {
  saveToServer();  // 重い
  rerenderList();  // 重い
});

// ✅ 見える反応を先に返し、重い処理はブラウザに描画させてから
button.addEventListener("click", async () => {
  showSpinner();  // まず即座に反応を返す(ここまでが INP の計測対象)

  // ブラウザに描画の機会を明け渡す。
  // scheduler.yield() 未対応の環境は次の行で代用できる:
  //   await new Promise((r) => setTimeout(r, 0));
  await scheduler.yield();

  await saveToServer();
  rerenderList();
});
  • サードパーティ JS を減らす — INP 悪化の犯人はたいてい自社コードではなく、後から足された計測・チャット・A/B テストのタグです
  • ハンドラで DOM を大量に触らない — React なら startTransition で緊急でない更新を後回しにする
  • まず視覚的な反応だけ返す — 押した感触さえ即座に返れば、裏の処理が 1 秒かかっても体感は悪くない

CLS はなぜ起きるのですか?

読み込み途中で後から入ってきた要素が、すでに表示されていたコンテンツを押しのけるためです。原因の大半は、寸法を指定していない画像と、高さを確保していない広告枠の 2 つです。読もうとした瞬間に文章が下へ飛ぶ、あの現象を数値化したものが CLS です。

CLS を防ぐ CSS
/* 広告枠: 埋まる前から高さを予約する。
   これをしないと広告が挿入された瞬間に本文が下へ飛び、CLS が跳ねる */
.ad-slot {
  min-height: 250px;  /* 300x250 を想定した最小値 */
  contain: layout;    /* 中の変化を外側へ波及させない */
}

/* 画像は width/height 属性を必ず付ける。
   属性があればブラウザが aspect-ratio を自動計算し、
   ダウンロード完了前に正しい場所を空けてくれる */
img {
  height: auto;
  max-width: 100%;
}

/* Web フォント差し替えによるズレ対策 */
@font-face {
  font-family: "MyFont";
  src: url("/f.woff2") format("woff2");
  font-display: swap;  /* 不可視テキスト(FOIT)を避ける */
  size-adjust: 97%;    /* 代替フォントとの字幅差を詰め、差し替え時のズレを減らす */
}

width / height 属性は「昔の書き方」ではありません。現代のブラウザはこの 2 つから aspect-ratio を自動計算し、画像のダウンロードが終わる前に正しい大きさの場所を空けます。CSS で max-width: 100% を当てていても、属性は付けたままにしてください。

CrUX と Lighthouse で数値が食い違うのはなぜですか?

測っているものが違うからです。CrUX は実ユーザーの過去 28 日間の計測値で、検索の評価に使われるのはこちらです。Lighthouse は手元で 1 回だけ、低速回線と低速 CPU を想定して行うシミュレーションです。「Lighthouse は 100 点なのに Search Console では不合格」は矛盾ではありません。

CrUX(フィールドデータ)と Lighthouse(ラボデータ)の違い
観点CrUX(実ユーザー)Lighthouse(シミュレーション)
測定対象実際の訪問者(オプトインした Chrome 利用者)実行した 1 台・1 回のみ
期間過去 28 日間の集計その瞬間
集計方法75 パーセンタイル値単一の実測値
回線・端末訪問者ごとにバラバラ(実態そのもの)低速 4G・CPU 4 倍遅い想定に固定
キャッシュリピーターは効いている常に空
INP測れる測れない(操作が無いため)。代わりに TBT を出す
検索の評価に使われるか使われる使われない
データが無い場合アクセスが少ないと表示されない常に出る

表の下から 3 行目が、混乱の最大の原因です。Lighthouse は INP を測れません。誰も操作していないからです。 代わりに TBT(Total Blocking Time)という代理指標を出しますが、これは INP そのものではありません。Lighthouse で 100 点を取っても、INP が不合格であることは普通にあります。

使い分けはこう考えてください。

  • CrUX = 通知表。合否はこれで決まる。ただし変化が反映されるまで最大 28 日かかる
  • Lighthouse = デバッガ。その場で原因を特定するための道具。点数は目的ではない

アクセス数が少ないサイトでは、そもそも CrUX にデータが集まらず「フィールドデータなし」になります。この場合はオリジン単位の値で代替されるか、評価対象から外れます。自前で実ユーザーの値を集めたい場合は次のようにします。

実ユーザー計測(RUM)
// 実ユーザーの値を自分で集める。ラボ値の推測から卒業する最短路。
// web-vitals ライブラリが 3 指標をそのまま報告してくれる
import { onLCP, onINP, onCLS } from "web-vitals";

function send(metric) {
  // sendBeacon なら離脱時でも取りこぼさない
  navigator.sendBeacon("/api/vitals", JSON.stringify(metric));
}

onLCP(send);
onINP(send);
onCLS(send);

このツールで Core Web Vitals が「判定できません」になるのはなぜですか?

本ツールは JavaScript を実行しないため、Core Web Vitals を自前で測れないからです。計測は PageSpeed Insights API に任せており、API キーが未設定の環境では推測値を出さず skip として正直に扱います。分からないものを分かったように見せないための仕様です。

本ツールは意図的に JavaScript を実行しません。理由は「JS なしで本文が読めるか」こそが AI 最適化の最重要指標であり、AI クローラーの多くも JS を実行しないからです。 素の HTML で診断することが、AI から見た実態の再現になります。

その代わり、Core Web Vitals の計測は Google の PageSpeed Insights API に委譲しています。API キーが設定されていない環境では、 本ツールは推測値を出しません。「判定できません」として明示します。 HTML から LCP を推し量ることは技術的には可能ですが、それは実測値ではなく当て推量です。当て推量を実測値のように見せるくらいなら、判定しないほうが誠実です。

一方で、TTFB・転送量・圧縮・キャッシュ設定・リダイレクト連鎖・画像最適化はJS 実行なしで正確に測れるため、 こちらは常に診断しています。LCP の ① と ③ の区間は、これらだけでもかなり追い込めます。

Core Web Vitals は SEO にどれくらい効きますか?

ランキング要因ではありますが、影響は小さいと考えてください。Google 自身、コンテンツの関連性のほうがはるかに重要だと明言しています。内容が拮抗したときの差になる程度です。ただし表示速度は直帰率とコンバージョンに直結するため、順位と無関係に改善する価値があります。

率直に言えば、Core Web Vitals を SEO 施策として最優先に据えるのは筋が悪いです。 内容の薄いページが LCP 1.2 秒になったところで上位には行きません。順位を動かしたいなら、まず記事そのものの質です。

それでも改善する価値があるのは、順位以外の効果が確実だからです。

  • 直帰率 — 表示が遅いページは、読まれる前に閉じられます。順位が上がっても意味がありません
  • コンバージョン率 — 特にモバイルの EC では、表示速度と購入率の相関が繰り返し報告されています
  • CLS は実害が大きい — 押すつもりのないボタンを押させる指標です。誤タップは信頼を直接損ないます

一次情報は web.dev の Web Vitals 解説 Chrome UX Report のドキュメント が正確です。自サイトの実ユーザーデータは Google Search Console の「ウェブに関する主な指標」レポートで確認できます。セキュリティ側の設定は セキュリティヘッダ 7 種の意味と設定 にまとめています。

関連する解説

自分のサイトは、実際どうなっているか

この記事で説明した項目は、すべて無料の診断ツールで実測できます。URL を入れるだけ・登録不要です。

無料で診断する