Core Web VitalsのLCPは画像の遅延読み込みで遅れる
ページの上にある大きな画像(ヒーロー画像)が Core Web Vitals の LCP(Largest Contentful Paint)の要素になっているときは、その <img> に loading="lazy" を付けないでください。画像の遅延読み込み(lazy loading)をまとめて設定したら、Lighthouse や PageSpeed Insights で LCP が遅いと指摘された、というときに確かめたい点です。検証したページでは、loading="lazy" を外すだけで、Lighthouse で測った LCP の中央値が 3,865ms から 3,267ms に縮みました。検証環境で Lighthouse と web-vitals を実際に動かし、出た数値をそのまま載せています。
検証環境は Ubuntu 24.04、Node.js 22、Lighthouse 13.6.0、web-vitals 6.2.3 です。ブラウザは Playwright 1.63.0 に入っている Chrome for Testing 153 を使いました。検証用のページは、検証環境で動かした小さなサーバー(localhost:8080、HTTP/1.1)から配っています。Lighthouse はモバイルの設定です。HTTP/2 や HTTP/3 のサーバー、Chrome 以外のブラウザ、PageSpeed Insights、実際の利用者のデータ(フィールドデータ)、公開しているサイトでの結果は試していません。
検証したページ:違いはヒーロー画像のloading="lazy"だけ
検証には、次のページを使いました。<head> で CSS と2つのスクリプト(それぞれ約80KB)を読み込みます。本文には見出しの下に大きな画像 hero.jpg(約130KB)があり、その下に小さな画像を4枚並べています。画面に出る中でいちばん大きいのは hero.jpg で、LCP の要素もこの画像になりました。
<head>
<!-- meta・title は省略 -->
<link rel="stylesheet" href="/style.css">
<script src="/app-a.js"></script>
<script src="/app-b.js"></script>
</head>
<body>
<h1>猫の手日記</h1>
<img src="/hero.jpg" alt="水色のもようの大きな画像" width="1200" height="675" loading="lazy">
<p>大きな画像の下に、ほかの記事の画像が並びます。</p>
<div class="grid">
<img src="/thumb1.jpg" alt="記事の画像 1" width="600" height="400">
<!-- thumb2〜thumb4 も同じ -->
</div>
</body>
比べたのは、このページと、hero.jpg の loading="lazy" だけを消したページの2つです。ページの <title> のほかは、何も変えていません。
<img src="/hero.jpg" alt="水色のもようの大きな画像" width="1200" height="675">
読み込み中の画面:下の画像が先に出て、大きな画像だけ空白
まず、2つのページを読み込むあいだの画面を比べます。Playwright で、回線と CPU を Lighthouse の devtools の設定と同じ値(遅延 562.5ms、下り 1,474.56Kbps、CPU を4倍遅く)にしてページを開き、読み込み開始から 0.5 秒ごとの画面を並べました。画面は、上のほうだけを切り出しています。ピンクの時刻は、LCP の時刻を過ぎて最初のコマです。

loading="lazy" を付けたページでは、3.0 秒の時点で、見出しと下の小さな画像は出ているのに、いちばん大きな画像の場所だけが白いままです。大きな画像が出たのは 4.0 秒のコマで、この読み込みでの LCP は 3,808ms でした。外したページでは、3.0 秒の時点で大きな画像の上のほうが描かれ始めていて、LCP は 3,208ms でした。
Lighthouseの「LCP request discovery」で指摘が出る
次に、loading="lazy" を付けたページに Lighthouse を実行しました。--throttling-method=devtools は、回線と CPU を実際に遅くしてページを読み込む設定です(既定の設定での結果は後の節で書きます)。結果の JSON から、LCP、「LCP request discovery」の項目、スクリプトと画像のリクエストの開始と終了の時刻(ミリ秒)、LCP の内訳を、小さなスクリプト(summarize.mjs)で抜き出しています。
$ npx lighthouse http://localhost:8080/before.html --throttling-method=devtools --only-categories=performance --output=json --output-path=out/before-devtools-1.json --chrome-flags="--headless=new --no-sandbox" --quiet
$ node summarize.mjs out/before-devtools-1.json
lighthouseVersion: 13.6.0 throttlingMethod: devtools
performance score: 84
LCP: 3887 ms (3.9 s, score 0.53)
lcp-discovery-insight: score=0 title="LCP request discovery"
[NG] fetchpriority=high should be applied
[ok] Request is discoverable in initial document
[NG] LCP resources shouldn’t use loading=lazy
LCP element: <img src="/hero.jpg" alt="水色のもようの大きな画像" width="1200" height="675" loading="lazy">
/style.css start= 660 end= 1233
/app-a.js start= 661 end= 2544
/app-b.js start= 663 end= 2553
/thumb1.jpg start= 664 end= 2147
/thumb2.jpg start= 664 end= 2154
/thumb3.jpg start= 2149 end= 3156
/thumb4.jpg start= 2156 end= 3172
/hero.jpg start= 2604 end= 3872
LCP breakdown: timeToFirstByte=15 resourceLoadDelay=2590 resourceLoadDuration=1269 elementRenderDelay=13
「LCP request discovery」は、LCP の画像の読み込みについて3つの点を確かめる項目です。3つのうち、「LCP resources shouldn’t use loading=lazy」(LCP のリソースに loading=lazy を使わない)の行と、fetchpriority の行が通りませんでした。--locale=ja を付けて日本語で実行すると、項目の名前は「LCP リクエストの検出」、loading=lazy の行は「LCP リソースで loading=lazy を使用しないでください」になります。

上のスクリーンショットは、既定の設定(--throttling-method を付けない)で実行したレポートです。こちらでは「Request is discoverable in initial document」(最初に受け取る HTML の中で見つけられる)の行も通りませんでした。devtools の設定では、この行は通っています。「fetchpriority=high should be applied」の行は、loading="lazy" を外したページでも通りません。この記事のページには、LCP の画像の取得を早める別の直し方である fetchpriority="high" を付けていないためです。
外す前後のLCP:Lighthouseとweb-vitalsで5回ずつ
数値のぶれも見るため、Lighthouse(devtools の設定)を2つのページで5回ずつ実行しました。
| 1回目 | 2回目 | 3回目 | 4回目 | 5回目 | 中央値 | |
|---|---|---|---|---|---|---|
| lazy あり | 3,887ms | 3,952ms | 3,859ms | 3,848ms | 3,865ms | 3,865ms |
| lazy なし | 3,267ms | 3,221ms | 3,302ms | 3,292ms | 3,242ms | 3,267ms |
LCP は中央値で 598ms 縮み、パフォーマンスの点数は 84(2回目だけ 83)から 87〜88 になりました。
Lighthouse とは別に、Google の web-vitals ライブラリの onLCP でも測りました。web-vitals は、実際の利用者のブラウザで Web Vitals の指標を測るライブラリで、README には Chrome と同じ方法で値を出すと書かれています。Playwright でページを開き、回線と CPU は、さきほどの画面の比較と同じ値にしています。LCP の内訳は、web-vitals の attribution 版が返す値です。
$ node measure.cjs 5
chromium 153.0.8010.12
before run1: LCP=3812ms element=html>body>img | TTFB=3 loadDelay=2518 loadDuration=1271 renderDelay=21
...
before median: 3804ms (min 3784, max 3812)
after run1: LCP=3188ms element=html>body>img | TTFB=2 loadDelay=592 loadDuration=2579 renderDelay=15
...
after median: 3184ms (min 3180, max 3188)
before が loading="lazy" ありのページ、after がなしのページです。web-vitals でも中央値は 3,804ms から 3,184ms と、約 0.6 秒縮みました。
web.dev の Core Web Vitals の解説では、LCP はページを読み込み始めてから 2.5 秒以内が「良好」の目安です。このページは、遅いモバイル回線を想定した測定では、外した後も 2.5 秒を超えています。
遅れた理由:画像の取得が始まるまでの待ち時間
LCP の内訳を見ると、どこで遅れたのかが分かります。web.dev の LCP の改善の解説では、LCP を次の4つに分けています。
- TTFB:ページを読み込み始めてから、HTML の最初の1バイトが届くまで
- リソース読み込みの遅延(Resource load delay):TTFB から、LCP の画像の取得が始まるまで
- リソースの読み込み時間:LCP の画像そのものの取得にかかる時間
- 要素のレンダリングの遅延:画像の取得が終わってから、画面に描かれるまで
Lighthouse の1回目の結果では、次のようになりました(単位はミリ秒)。
| TTFB | 読み込みの遅延 | 読み込み時間 | レンダリングの遅延 | |
|---|---|---|---|---|
| lazy あり | 15 | 2,590 | 1,269 | 13 |
| lazy なし | 5 | 660 | 2,585 | 17 |
loading="lazy" を付けたページでは、hero.jpg の取得が始まるまでに 2.6 秒近く待っています。外したページでは、この待ちが 0.7 秒ほどに縮みました。web-vitals の内訳(loadDelay)でも、5回とも 2,518〜2,527ms と 584〜593ms で、同じ傾向です。
次のグラフは、Lighthouse の1回目の結果から、スクリプトと画像ごとにリクエストの開始から終了までを横棒にしたものです。さきほどの画面の比較とは別の読み込みなので、LCP の値は少し違います。

loading="lazy" ありのページでは、hero.jpg を取りに行き始めたのは 2,604ms で、<head> の2つのスクリプトの取得が終わった(2,544ms と 2,553ms)すぐ後でした。loading を付けていない下の4枚の画像のうち2枚は、スクリプトと同じ 664ms から取得が始まっていて、ページで後ろにある画像のほうが先に読み込まれています。外したページでは、hero.jpg の取得はスクリプトと同じ 667ms に始まりました。
web.dev の LCP の改善の解説には、loading="lazy" を付けた画像は、レイアウトによって画像が画面内にあると確かめられるまで読み込まれないので、付けない場合より読み込みが遅く始まることがある、と書かれています。同じ解説は、LCP の画像を遅延読み込みにすると、読み込みの遅延が必ず増えて LCP が悪くなるので、決して遅延読み込みにしないように、と注意しています。
外したページでは、取得そのものにかかる時間(読み込み時間)は 1,269ms から 2,585ms に延びました。グラフのとおり、取得がスクリプトやほかの画像の取得と並んで進んでいます。それでも始まりが 1.9 秒早くなったので、取得が終わる時刻は 3,872ms から 3,248ms と、約 0.6 秒早まりました。
既定のLighthouseでは、lazyありのほうがLCPが短く出た
--throttling-method を付けない既定の設定(simulate)でも、2つのページで3回ずつ Lighthouse を実行しました。次の出力は、それぞれの結果の JSON から summarize.mjs で LCP と点数を抜き出したものです。Lighthouse のスロットリング(回線と CPU を遅くする設定)の説明によると、この設定は、回線を遅くせずに読み込んだときの記録をもとに、遅い回線での読み込みを計算で見積もります。PageSpeed Insights も同じ設定だと書かれています。
before simulate run1: LCP: 2932 ms (2.9 s) performance score: 94
before simulate run2: LCP: 2930 ms (2.9 s) performance score: 94
before simulate run3: LCP: 2927 ms (2.9 s) performance score: 94
after simulate run1: LCP: 3602 ms (3.6 s) performance score: 89
after simulate run2: LCP: 3601 ms (3.6 s) performance score: 90
after simulate run3: LCP: 3602 ms (3.6 s) performance score: 90
こちらでは逆に、loading="lazy" ありのページ(before)のほうが LCP が 0.7 秒短く、点数も高く出ました。実際に回線を遅くして読み込んだ devtools の設定の Lighthouse、web-vitals、読み込み中の画面の記録では、どれも loading="lazy" ありのほうが遅かったのとは、向きが反対です。一方で「LCP request discovery」の「LCP resources shouldn’t use loading=lazy」の行は、既定の設定でも loading="lazy" ありのページでだけ通りませんでした。
Lighthouse の説明には、既定の設定はたいていのサイトで devtools の設定と同じくらいか、それより正確だが、うまく見積もれない場合もあると書かれています。この検証のページでは、既定の設定の LCP の数値からは loading="lazy" の悪い影響が読み取れませんでした。既定の設定の Lighthouse(PageSpeed Insights も同じ方式です)の LCP の数値だけを見て判断せず、「LCP request discovery」の loading=lazy の行も確かめると、見落としを防げます。
loading="lazy"を残すのは、最初の画面の外にある画像
MDN の img 要素のリファレンスによると、loading="lazy" は、画像が画面からブラウザの決めた距離まで近づくまで読み込みを遅らせる値です。必要になることがほぼ確実になるまで、画像の通信量を使わずに済むので、たいていの場合は性能がよくなるとも書かれています。既定値は eager(画面内にあるかどうかに関係なく、すぐに読み込む)です。
外すのは、最初の画面に出て LCP の要素になる画像の loading="lazy" だけです。web.dev の遅延読み込みのしすぎについての解説でも、最初の画面に入る画像はすぐに読み込み、それ以外の画像は積極的に遅延読み込みにすると、読み込む量を減らしながら Core Web Vitals もよくできる、とまとめています。どの画像が LCP の要素になったかは、Lighthouse の「LCP request discovery」の下に出る <img> のタグか、web-vitals の attribution 版が返す要素(この検証では html>body>img)で確かめられます。





