レイアウト シフトをデバッグする

レイアウトのずれを特定して修正する方法について学びます。

Katie Hempenius
Katie Hempenius

この記事の前半では、レイアウト シフトのデバッグ用ツールについて説明します。後半では、レイアウト シフトの原因を特定する際の考え方について説明します。

ツール

Layout Instability API

Layout Instability API は、レイアウトのずれを測定して報告するためのブラウザ メカニズムです。DevTools を含む、レイアウト シフトのデバッグ用ツールはすべて、最終的には Layout Instability API 上に構築されています。ただし、Layout Instability API を直接使用すると、柔軟性が高いため強力なデバッグ ツールになります。

用途

Cumulative Layout Shift(CLS) を測定するコード スニペットは、レイアウト シフトのデバッグにも使用できます。次のスニペットは、レイアウトのずれに関する情報をコンソールに記録します。このログを調べると、レイアウト シフトが発生した日時、場所、方法に関する情報を確認できます。

let cls = 0;
new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    if (!entry.hadRecentInput) {
      cls += entry.value;
      console.log('Current CLS value:', cls, entry);
    }
  }
}).observe({type: 'layout-shift', buffered: true});

このスクリプトを実行する際は、次の点に注意してください。

  • buffered: true オプションは、PerformanceObserver がオブザーバーの初期化前に作成されたパフォーマンス エントリについて、ブラウザのパフォーマンス エントリ バッファをチェックする必要があることを示します。その結果、PerformanceObserver は初期化の前後に発生したレイアウト シフトを報告します。コンソール ログを調べる際は、この点に注意してください。レイアウト シフトが最初に急増した場合、多数のレイアウト シフトが突然発生したのではなく、レポートのバックログが反映されている可能性があります。
  • パフォーマンスに影響しないように、PerformanceObserver はメインスレッドがアイドル状態になるまで待機してから、レイアウトのずれを報告します。そのため、メインスレッドの負荷によっては、レイアウト シフトが発生してからコンソールにログに記録されるまでに若干の遅延が生じることがあります。
  • このスクリプトは、ユーザー入力から 500 ミリ秒以内に発生したレイアウト シフトを無視するため、CLS にはカウントされません。

レイアウト シフトに関する情報は、LayoutShift インターフェースと LayoutShiftAttribution インターフェースの 2 つの API を組み合わせて報告されます。これらのインターフェースについては、以降のセクションで詳しく説明します。

LayoutShift

各レイアウト シフトは LayoutShift インターフェースを使用して報告されます。エントリの内容は次のようになります。

duration: 0
entryType: "layout-shift"
hadRecentInput: false
lastInputTime: 0
name: ""
sources: (3) [LayoutShiftAttribution, LayoutShiftAttribution, LayoutShiftAttribution]
startTime: 11317.934999999125
value: 0.17508567530168798

上記のエントリは、3 つの DOM 要素の位置が変更されたレイアウト シフトを示しています。このレイアウト シフトのレイアウト シフト スコアは 0.175 でした。

レイアウト シフトのデバッグに最も関連する LayoutShift インスタンスのプロパティは次のとおりです。

プロパティ 説明
sources sources プロパティには、レイアウト シフト中に移動した DOM 要素のリストが表示されます。この配列には最大 5 つのソースを含めることができます。レイアウト シフトの影響を受ける要素が 5 つを超える場合は、レイアウトの安定性への影響に基づいて、レイアウト シフトの原因として上位 5 つが報告されます。この情報は、LayoutShiftAttribution インターフェースを使用して報告されます(後で詳しく説明します)。
value value プロパティは、特定のレイアウト シフトのレイアウト シフト スコアを報告します。
hadRecentInput hadRecentInput プロパティは、ユーザー入力から 500 ミリ秒以内にレイアウト シフトが発生したかどうかを示します。
startTime startTime プロパティは、レイアウト シフトが発生したタイミングを示します。startTime はミリ秒単位で、ページの読み込みが開始された時刻を基準に測定されます。
duration duration プロパティは常に 0 に設定されます。このプロパティは PerformanceEntry インターフェースから継承されます(LayoutShift インターフェースは PerformanceEntry インターフェースを拡張します)。ただし、レイアウト シフト イベントには継続時間の概念が適用されないため、0 に設定されます。PerformanceEntry インターフェースの詳細については、仕様をご覧ください。

LayoutShiftAttribution

LayoutShiftAttribution インターフェースは、単一の DOM 要素の単一シフトを記述します。レイアウト シフト中に複数の要素がシフトする場合、sources プロパティには複数のエントリが含まれます。

たとえば、次の JSON は、<div id='banner'> DOM 要素が y: 76 から y:246 に下方にシフトするという、1 つのソースによるレイアウト シフトに対応しています。

// ...
  "sources": [
    {
      "node": "div#banner",
      "previousRect": {
        "x": 311,
        "y": 76,
        "width": 4,
        "height": 18,
        "top": 76,
        "right": 315,
        "bottom": 94,
        "left": 311
      },
      "currentRect": {
        "x": 311,
        "y": 246,
        "width": 4,
        "height": 18,
        "top": 246,
        "right": 315,
        "bottom": 264,
        "left": 311
      }
    }
  ]

node プロパティは、シフトされた HTML 要素を識別します。DevTools でこのプロパティにカーソルを合わせると、対応するページ要素がハイライト表示されます。

previousRect プロパティと currentRect プロパティは、ノードのサイズと位置を報告します。

  • x 座標と y 座標は、要素の左上隅の x 座標と y 座標をそれぞれ報告します。
  • width プロパティと height プロパティは、要素の幅と高さをそれぞれ報告します。
  • toprightbottomleft プロパティは、要素の特定のエッジに対応する x 座標値または y 座標値を報告します。つまり、top の値は y と等しく、bottom の値は y+height と等しい。

previousRect のすべてのプロパティが 0 に設定されている場合、要素がビュー内に移動したことを意味します。currentRect のすべてのプロパティが 0 に設定されている場合、要素がビューの外側にシフトされていることを意味します。

これらの出力を解釈する際に最も重要なことは、[ソース] としてリストされている要素は、レイアウト シフト中にシフトされた要素であるということです。ただし、これらの要素は、レイアウトの不安定性の「根本原因」に間接的にしか関連していない可能性があります。次に例を示します。

例 1

このレイアウト シフトは、1 つのソース(要素 B)で報告されます。ただし、このレイアウトのずれの根本原因は、要素 A のサイズの変更です。

要素のサイズ変更によるレイアウト シフトの例

例 2

この例のレイアウト シフトは、要素 A と要素 B の 2 つのソースで報告されます。このレイアウト シフトの根本原因は、要素 A の位置の変更です。

要素の位置の変更によってレイアウトがずれる例

例 3

この例のレイアウト シフトは、1 つのソース(要素 B)で報告されます。要素 B の位置を変更した結果、このレイアウト シフトが発生しました。

要素の位置の変更によってレイアウトがずれる例

例 4

この例では、要素 B のサイズは変更されますが、レイアウト シフトは発生しません。

要素のサイズが変更されてもレイアウトがずれない例

Layout Instability API によって DOM の変更が報告される方法のデモをご覧ください。

DevTools

[パフォーマンス] パネル

DevTools の [パフォーマンス] パネルの [エクスペリエンス] ペインには、特定のパフォーマンス トレース中に発生したすべてのレイアウト シフトが表示されます。ユーザー操作から 500 ミリ秒以内に発生したレイアウト シフトは CLS にカウントされないため、表示されます。[エクスペリエンス] パネルで特定のレイアウト シフトにカーソルを合わせると、影響を受ける DOM 要素がハイライト表示されます。

DevTools の [Network] パネルに表示されたレイアウト シフトのスクリーンショット

レイアウト シフトの詳細を確認するには、レイアウト シフトをクリックして [概要] ドロワーを開きます。要素のサイズの変更は [width, height] 形式で、要素の位置の変更は [x,y] 形式で表示されます。Had recent input プロパティは、ユーザー操作から 500 ミリ秒以内にレイアウト シフトが発生したかどうかを示します。

レイアウト シフトのデベロッパー ツールの [Summary] タブのスクリーンショット

レイアウト シフトの継続時間については、[イベントログ] タブを開きます。レイアウト シフトの所要時間は、[エクスペリエンス] ペインで赤いレイアウト シフトの長さを確認することで概算できます。

レイアウト シフトに関するデベロッパー ツールの [Event Log] タブのスクリーンショット

[パフォーマンス] パネルの使用方法については、パフォーマンス分析のリファレンスをご覧ください。

レイアウト シフト領域をハイライト表示

レイアウト シフト領域をハイライト表示すると、ページで発生するレイアウト シフトの場所とタイミングをすばやく把握できます。

DevTools でレイアウト シフト領域を有効にするには、[設定] > [その他のツール] > [レンダリング] > [レイアウト シフト領域] に移動し、デバッグするページを更新します。レイアウト シフトが発生した領域は、紫色でハイライト表示されます。

レイアウトのずれの原因を特定するための考え方

レイアウトのずれがいつ、どのように発生したかにかかわらず、以下の手順でレイアウトのずれの原因を特定できます。これらの手順は Lighthouse の実行で補完できますが、Lighthouse で特定できるのは、最初のページ読み込み時に発生したレイアウト シフトのみです。また、Lighthouse は、明示的な幅と高さがない画像要素など、レイアウトのずれの原因の一部についてのみ、推奨事項を提供できます。

レイアウト シフトの原因を特定する

レイアウトのずれは、次のイベントによって発生する可能性があります。

  • DOM 要素の位置の変更
  • DOM 要素のサイズの変更
  • DOM 要素の挿入または削除
  • レイアウトをトリガーするアニメーション

特に、シフトされた要素の直前の DOM 要素は、レイアウト シフトの「原因」となる可能性が高い要素です。したがって、レイアウト シフトが発生した理由を調査する際は、次の点を考慮してください。

  • 前の要素の位置やサイズに変更はありましたか?
  • シフトされた要素の前に DOM 要素が挿入または削除されたか?
  • 移動された要素の位置が明示的に変更されましたか?

前の要素がレイアウト シフトの原因ではなかった場合は、他の前の要素と近くの要素を検討して検索を続けます。

また、レイアウトのずれの方向と距離から、根本原因に関するヒントを得ることができます。たとえば、下方向への大きなシフトは DOM 要素の挿入を示すことが多く、1 ピクセルまたは 2 ピクセルのレイアウト シフトは、競合する CSS スタイルの適用や、ウェブフォントの読み込みと適用を示すことが多くあります。

フォント交換によって発生したレイアウト シフトを示す図
この例では、フォントを入れ替えたためにページ要素が 5 ピクセル上方にシフトしました。

レイアウト移動イベントを引き起こす最も一般的な動作は次のとおりです。

要素の位置の変更(他の要素の移動によるものではない)

このような変更は、次のようなことが原因で発生することがよくあります。

  • 遅れて読み込まれるスタイルシートや、以前に宣言されたスタイルを上書きするスタイルシート。
  • アニメーションと遷移効果。

要素の寸法の変更

このような変更は、次のようなことが原因で発生することがよくあります。

  • 遅れて読み込まれるスタイルシートや、以前に宣言されたスタイルを上書きするスタイルシート。
  • width 属性と height 属性のない画像と iframe が、「スロット」のレンダリング後に読み込まれる。
  • テキストのレンダリング後にフォントを切り替える width 属性または height 属性のないテキスト ブロック。

DOM 要素の挿入または削除

多くの場合、これは次のような原因で発生します。

  • 広告やその他のサードパーティの埋め込みの挿入。
  • バナー、アラート、モーダルの挿入。
  • 既存のコンテンツの上に追加のコンテンツを読み込む無限スクロールなどの UX パターン。

レイアウトをトリガーするアニメーション

一部のアニメーション効果はレイアウトをトリガーする可能性があります。一般的な例としては、CSS の transform プロパティを使用するのではなく、topleft などのプロパティをインクリメントして DOM 要素を「アニメーション化」する場合などがあります。詳細については、高パフォーマンスの CSS アニメーションを作成する方法をご覧ください。

レイアウト シフトの再現

再現できないレイアウトのずれは修正できません。サイトのレイアウトの安定性を把握するための最も簡単で効果的な方法の一つは、レイアウトの変化をトリガーすることを目的として、5 ~ 10 分間サイトを操作することです。コンソールを開いたまま、Layout Instability API を使用してレイアウトのずれを報告します。

レイアウトのずれが見つけにくい場合は、別のデバイスと接続速度でこの演習を繰り返すことを検討してください。特に、接続速度が遅い場合は、レイアウトのずれを簡単に特定できます。また、debugger ステートメントを使用すると、レイアウト シフトを簡単にステップスルーできます。

new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    if (!entry.hadRecentInput) {
      cls += entry.value;
      debugger;
      console.log('Current CLS value:', cls, entry);
    }
  }
}).observe({type: 'layout-shift', buffered: true});

最後に、開発環境で再現できないレイアウトの問題については、Layout Instability API と任意のフロントエンド ロギング ツールを併用して、これらの問題に関する詳細情報を収集することを検討してください。ページ上で最も大きく移動した要素を追跡する方法のコード例をご覧ください。