LighthouseとPageSpeedの「Layout shift culprits」を修正する
ページ読み込み中に何が動いたのか、Lighthouseが指摘した要素をどう確認し、画像、フォント、広告、バー、遅れて読み込まれるCSSによるシフトをどう修正するかを解説します。
PageSpeed Insightsで「Layout shift culprits」が赤色になるのはなぜでしょうか?
読み込み中のレイアウトシフトの合計がCLS 0.1以上になると、「Layout shift culprits」は赤色で表示されます。「ユーザーの操作がない状態で要素が移動すると、レイアウトシフトが発生します。」Lighthouseでは以前「Avoid large layout shifts」と呼ばれていました。ここには移動した要素が一覧表示され、原因が推測されます。遅れて読み込まれる要素のために領域を確保してください。画像にはwidthとheightを、広告枠にはheightを指定します。
手っ取り早く修正したい場合は、画像、Webフォント、広告や埋め込み要素、またはバナーへ移動してください。
「Layout shift culprits」の表はどう読めばよいでしょうか?
Lighthouseは、スマートフォン画面サイズでのページ読み込みを1回記録します。そして、発生したレイアウトシフトを1秒以内のまとまり(バースト)ごとに、バーストあたり1つの表に一覧表示します。最もスコアの悪いバーストが0.1以上になると通知は赤色になり、それ未満ならグレーになります。グレーであっても0を目指してください。0.1未満のシフトであっても、訪問者が読んでいるテキストは動いてしまいます。
WordPressの料理ブログを模して作成したレシピページでは、見出しにWebフォントを使い、ニュースレタープラグイン、ヘッダー下の広告、レシピカードプラグインを導入したところ、1回のバーストで0.241を記録し、通知は赤色になりました。

PageSpeed Insightsと同様のシミュレートされたスロットリング環境下、Lighthouse 13.5(モバイル)でのレシピページの表示。
- Totalはバーストのスコアです。最もスコアの悪いバーストのTotalが、LighthouseがレポートするCLSになります。
- Elementはそのシフトで最も大きく動いた要素です。多くは
<main>のような大きなコンテナです。それを押し下げた要素は、通常その上部にあります。 - Layout shift scoreは、その1回のシフトがTotalに占めるスコアです。
- 行の下に表示される行は、Lighthouseが推測した原因です。画像に対する「Unsized image element」や、フォントファイルに対する「Web font」などが表示されます。1つの行に両方が表示されることもあれば、どちらも表示されないこともあります。
このレシピページでは、4回のシフトのうち3回でLighthouseが誤った画像を原因として挙げていました。修正を加える前に、実際に動いた要素を確認してください。
読み込み中にページがガタつくのはなぜでしょうか?
訪問者が文章を読み始めた瞬間にテキストがずれたり、リンクをタップしようとしたら別の要素がそこへ滑り込んできたりします。
ブラウザはその時点で手元にあるデータを使ってページを描画します。あらかじめ領域が確保されていないコンテンツが遅れて届くと、その下にあるコンテンツが押し出されます。サイズ指定のない画像は高さ0から始まり、Webフォントは見出しをfallbackフォントよりも幅広く描画します。これらの移動の1つひとつがレイアウトシフトであり、画面のどれだけの領域がどれだけの距離移動したかによってスコア化されます。1秒未満の間隔で起きたシフトは1つのバーストにまとめられ、CLSは最もスコアの悪いバーストの値となります。そのため、わずかなシフトでも連続して発生するとスコアが積み重なります。
今回のレシピページでは、個々の原因によるシフトはどれも単体では0.1未満でしたが、4つすべてが1つのバースト内で発生しました:
| レシピページでの原因 | 単体でのCLS | 修正後 |
|---|---|---|
| 見出しのWebフォント | 0.035 | 0 |
| widthとheightがないレシピ写真 | 0.036 | 0 |
| ヘッダー上のニュースレターバー | 0.058 | 0 |
| 空き枠に表示される広告 | 0.054 | 0 |
| 4つの合計 | 0.241 | 0 |
4つ合わせたスコアは各行の合計よりも大きくなっています。ページ全体で見た場合、フォントと写真の双方が画面のより広い領域を動かすためです。これら4つをすべて修正したことで、FCP、LCP、Total Blocking Timeは変わらないまま、モバイルのPerformanceスコアは87から100に向上しました。CLSはこのスコアの4分の1を占めますが、Googleのランキング評価に使われるのは、実際の訪問者が体験するCLSです。

修正前、初回ペイント時

修正前、2秒後

修正後、初回ペイント時

修正後、2秒後
Lighthouseのスマートフォンサイズで表示したレシピページ。破線の枠は、初回ペイント時に写真があった位置を示しています。
Lighthouseが指摘した要素は本当に原因でしょうか?
Lighthouseはタイミングから原因を推測します。シフトの直前にダウンロードが完了したWebフォントや、シフトの直後にブラウザが描画した画像などがそれにあたります。そのため、単に下へ押し下げられただけの画像が「Unsized image element」として挙げられることがあります。このレシピページのアイキャッチ画像にはwidth="1200" height="675"が指定されていましたが、Lighthouseはフォント、ニュースレターバー、広告によるシフトの原因としてもこの画像を挙げていました。下に行が表示されていない項目については、ステップ2から進めてください。
- 指摘された画像にサイズは指定されていますか? 同じレポートのDiagnostics(診断)にある「Image elements do not have explicit width and height」に、サイズ指定のない画像がリストアップされています。DevToolsでは、領域が確保されている場合、画像のComputedタブのaspect-ratioに比率(auto 1200 / 675)が表示され、確保されていない場合はautoのみが表示されます。テーマ側でwidth: autoが設定されている場合(後述のUnsized image elementを参照)を除き、サイズ指定がある画像がシフトの原因になることはありません。その場合は行の下にある別の項目(多くは「Web font」)を確認するか、ステップ2に進んでください。
- その瞬間に何が表示されましたか? Performanceパネルで再読み込みを記録してください。Layout shiftsトラックでは、各シフトが紫色の菱形で表示されます。菱形にカーソルを合わせると、スコアと、移動した領域がハイライトされたその瞬間のスクリーンショットが表示されます。レシピページの0.0583のシフトでは、上部に現れたばかりの緑色のニュースレターバーが確認できます。スクロール中のシフトを調べるには、Recordをクリックし、スクロールしてからStopをクリックします。
- 原因と疑われるリクエストをブロックして再読み込みします。 Networkパネルで、フォントファイルや広告タグなど原因と疑われるリクエストを右クリックし、「Block requests」から「Block request URL」を選択して再度記録します。消えた菱形があれば、それがそのリクエストに起因するシフトです。レシピページでフォントをブロックしたところ、1つのシフトが消え、残り3つが残りました。確認後は、Request conditionsドロワーでブロックのチェックを外してください。

レシピページでのステップ2:Chrome 155、Slow 4G、CPU 4倍スローダウン。
DevToolsを使わなくても、CoreDashのcRUMで同様の検証が可能です。ページのChromeトレースと実際の訪問者が体験したデータを並べて表示し、動いた要素を特定して、その直前に訪問者が行った操作を表示します。下のスクリーンショットは別のページのものです。まずは「Chrome trace」と「Real users」から確認してください。

acme-pagespeed.comの特定ページにおけるCoreDash cRUM(モバイル)。
実際の訪問者で動いているのはどの要素でしょうか?
Lighthouseは新規訪問者としてページを1回読み込むだけであり、スクロールやタップを行うことはありません。field dataには、スクロールしたときに表示される記事途中の広告や、スクロール時に縮小するヘッダーなど、実際のすべてのアクセスが記録されています。Lighthouseで問題がない(またはグレー表示である)にもかかわらず、Search ConsoleでCLSが「不良」と判定される場合、シフトは通常、読み込み完了後に発生しています。
CoreDashでデバイスをmobileに設定し、CLSを「Attribution Element (CLS)」でグループ化すると、どの要素が動いたかを確認できます。「More dimensions」にある「Last interaction (CLS)」を見れば、それがスクロール後に発生したかどうかがわかります。これらの要素を何が押し下げたのかを特定するには、スクロールしながらPerformanceパネルでそのページを記録します(上記の検証手順のステップ2)。

モバイルでのCoreDash(p75):最も高いCLSから順に着手します。
「Layout shift culprits」はどう修正すればよいでしょうか?
上記の検証で原因を確認したら、最もTotalが大きい表の中で、最大のシフトから着手してください。
Unsized image element
サイズ指定のない画像は高さ0から始まり、読み込まれた瞬間にその下にあるすべての要素を押し下げます。WordPressはメディアライブラリの画像にwidthとheightを自動付与しますが、テーマやページビルダー、スライダー、プラグインなどが独自に出力する画像には、レシピカードの写真のようにそれらが欠落していることがよくあります。テーマを変更できない場合は、WP Rocketで不足しているサイズを付与できます(「Media」の「Add missing image dimensions」)。テンプレート内では、以下のように直接記述します:
<img src="shakshuka.jpg" width="800" height="600" alt="Shakshuka in the pan">
img { max-width: 100%; height: auto; } ブラウザはこの2つの属性からアスペクト比を計算し、ファイルが届く前にあらゆる画面幅で適切な高さを確保します。テーマのCSSにwidth: autoがあると、この比率が無効化されてしまいます(auto sizing images)。Shopifyではimage_tagフィルターでサイズが付与されます。独自に<img>を出力するテーマやアプリでは、width="{{ image.width }}" height="{{ image.height }}"の指定が必要です。Next.jsの場合、next/imageにはwidthとheightを指定するか、サイズが設定された親要素の内部でfillを使用します。
Web font
見出しはまずfallbackフォントで表示され、その後にWebフォントで表示されます。Webフォントの文字幅が広いか狭いかによって改行位置が変わり、その下にあるすべての要素が動いてしまいます。WordPress(「追加CSS」)やShopify(「コードを編集」内のテーマスタイルシート。ただしテーマ更新で上書きされる可能性があります)では、ローカルフォントの寸法をWebフォントに合わせてリサイズするfallback用の@font-faceを追加します。Next.jsでは、next/fontが適合するfallbackを自動で追加します。以下は、レシピページの見出しフォントの設定例です:
@font-face {
font-family: "Young Serif Fallback";
src: local("Times New Roman"), local("Liberation Serif");
size-adjust: 131%;
ascent-override: 79.85%;
descent-override: 27.94%;
line-gap-override: 0%;
}
h1 { font-family: "Young Serif", "Young Serif Fallback", serif; } 計算ツールで算出したsize-adjust 122%ではまだ0.029のCLSが残っていましたが、131%に設定したところ0になりました。Font displayでは、Android用の設定や独自の値の求め方、またシフトを防いで初回訪問者にfallbackフォントを表示するfont-display: optionalについて解説しています。
広告、動画、その他の埋め込み要素
広告スクリプトは、オークション完了後にスロットへiframeを挿入します。スロットが空だった場合、その下にあるすべての要素が広告の高さ分だけ押し下げられます。State of Web Vitalsによると、モバイルにおいて広告ネットワークのないサイトの89%がCLSに合格しているのに対し、2つ以上のネットワークを導入しているサイトでは73%にとどまります。これは相関関係を示しています。
テーマまたは広告プラグインで、広告コンテナに最も頻繁に表示されるサイズの高さを指定し、iframeをブロック要素にしてください:
.ad-slot { min-height: 50px; }
.ad-slot iframe { display: block; } iframeはデフォルトでインライン要素として配置されるため、今回のレシピページでもiframeをブロック要素にするまでは、確保したスロットが8ピクセル余分に広がっていました。広告が配信されない場合でも、スロットは開いたまま維持してください。枠を閉じてしまうと、広告を挿入したときと同じだけのレイアウトシフトが発生します。AdSenseの自動広告はGoogleが決定した位置に挿入されます。ファーストビュー内の領域を除外し(広告設定の「除外エリア」)、サイズを指定したスロットに手動広告ユニットを配置してください。
YouTubeや地図などの埋め込み要素もiframeです。iframeではwidthとheightの属性から比率が計算されないため、height: autoを指定すると約150ピクセルまで縮んでしまいます。iframeにaspect-ratioを指定するか、クリック時にiframeへ切り替わるサイズ指定済みのサムネイル(ファサード)を使用してください(YouTube、Google Maps):
.video-embed iframe { width: 100%; height: auto; aspect-ratio: 16 / 9; } スクリプトによって挿入されるバーやバナー
スクリプトがヘッダー上部に挿入するニュースレターバーや送料無料告知バーは、ページ全体を下に押し下げます。
まずはプラグインやアプリのレイアウト設定を確認してください。下部バー、フロートボックス、ポップアップであればページの上に重なって表示されるため、コンテンツを動かしません。OneTrustやCookieYesなどの同意管理ツールも同様のレイアウトを提供しています。なお、LighthouseはCookieのない状態で開始するため、実行のたびにこれらのバナーを検出します。どうしてもヘッダーの上にバーを配置する必要がある場合は、テーマ側であらかじめ領域を確保しておきます。高さには、スマートフォン幅のComputedタブに表示される値を指定してください:
<div id="promo-slot" style="min-height: 48px"></div> 一部のアクセスでのみ表示するバーの場合は、フローティング表示にするか、サーバー側でHTMLに直接レンダリングしてください。Google Tag Managerタグから配信される目に見えるコンテンツは、必ず初回ペイントの後に届きます。これらはテンプレート側に直接記述してください。Shopifyの場合、レビューの星評価や告知バーなどのウィジェットを挿入するアプリでは、アプリブロックを配置するコンテナに高さを指定する必要があります。
初回ペイント後に読み込まれるCSS
media="print"ハックで読み込まれるスタイルシートや、最適化プラグインによって遅延・非同期読み込みされたスタイルシートは、ページが画面に表示された後に届き、既存の要素のサイズを変更します。例えばヘッダーが広がれば、その下にあるすべての要素が押し下げられます。プラグイン側でCSSの遅延や非同期読み込みの対象からそのファイルを除外するか、テーマ側でheadタグ内に通常の<link rel="stylesheet">を記述して読み込んでください。
JavaScriptでレンダリングされるアニメーション、スライダー、ブロック
これらは通常、テーマ、プラグイン、またはアプリが原因です。まずはそれらの設定を確認してください。設定項目がない場合は、レポートで指摘された要素と以下のコードを開発者に共有してください。
top、left、height、marginのアニメーションは、フレームごとにレイアウトの再計算を発生させます。Lighthouseでは原因の行がない複数の小さな行として表示され、Diagnosticsの「Avoid non-composited animations」にその要素名が挙げられます。代わりにtransformをアニメーション化してください。周囲の要素に影響を与えずその要素のピクセルだけを動かすため、ブラウザはレイアウトシフトとしてカウントしません。
.promo { position: fixed; top: 0; left: 0; right: 0;
transform: translateY(-100%); transition: transform .3s; }
.promo.is-open { transform: translateY(0); } スライダーはスクリプトが実行されるまでスライドが縦に積み重なって表示されます。スクリプトが準備完了クラスを付与するまでは、最初のスライドのみを表示するように維持します(使用しているスライダーに応じたクラス名に変更してください):
.slider:not(.is-ready) > :not(:first-child) { display: none; } レビューや関連商品など、読み込み後にJavaScriptがレンダリングするブロックには、最終的な高さを確保した空のコンテナを用意するか、サーバー側でレンダリングする必要があります。スクロール時のアニメーションについては、scroll-triggered animationsおよびCSS transitionsを参照してください。
編集できないコードによるシフト
広告クリエイティブ、レイアウト設定のないアプリ、サードパーティのスクリプトが何を挿入するかを直接制御することはできません。通常表示される高さをあらかじめ確保するか、ファーストビューより下に配置するか、あるいは自身の領域内で収まる別のツールに置き換えてください。
修正によってCLSは0になったでしょうか?
キャッシュプラグインとCDNをクリアした状態で、モバイルのLighthouseで修正結果を確認してください。すべてのシフトが解消されると、「Layout shift culprits」は合格(passed)として表示され、CLSは0になります。再読み込み時とスクロール時のそれぞれでPerformanceパネルを記録した際、Layout shiftsトラックは空になっているはずです。CoreDashでは次回のアクセスから要素ごとの新しいCLSが反映されますが、Search Consoleの反映には最大28日かかります。

修正前

修正後
4つの修正の適用前と適用後:Lighthouse 13.5、モバイル、シミュレートされたスロットリング。
直接手を入れられない部分からシフトが発生することもあります。広告パートナーが配信する広告枠、ウィジェットを注入するアプリ、スクロール後に実際の訪問者にのみ発生するシフトなどです。Core Web Vitals パワーアワーでは、実際のfield dataとページの記録を精査し、どのコンテナにどれだけの高さを確保すべきかを診断・決定します。


なぜ修正が効かないのか?
CDNがまだ古いHTMLを配信している、あるいは修正がデスクトップ用テンプレートにだけ入り、モバイルは別のテンプレートを使っている、ということがあります。URLと問題をお送りいただければ、候補の日時をご返信します。まずfield dataを確認し、1時間サイトを一緒に見て、1日以内に調査メモを書面でお送りします。€350(税別)。
URLを送る修正を実装する開発者がいませんか? サイトのコピー上で私が作業します。