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

レイアウト シフトを特定して修正する方法について説明します。

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 インターフェースの詳細については、specをご覧ください。

LayoutShiftAttribution

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

たとえば、以下の JSON は 1 つのソースのレイアウト シフト(y: 76 から y:246 への <div id='banner'> DOM 要素のダウンシフト)に対応しています。

// ...
  "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 にカウントされません。[Experience] パネルで特定のレイアウト シフトにカーソルを合わせると、影響を受ける DOM 要素がハイライト表示され、

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

レイアウト シフトの詳細を表示するには、レイアウト シフトをクリックして [Summary] ドロワーを開きます。要素のディメンションに対する変更は、[width, height] の形式で一覧表示されます。要素の位置の変更は、[x,y] の形式を使用して一覧表示されます。[HadRecent input] プロパティは、ユーザー操作から 500 ミリ秒以内にレイアウト シフトが発生したかどうかを示します。

レイアウト シフトに関する DevTools の [Summary] タブのスクリーンショット

レイアウト シフトの期間については、[Event Log] タブを開いてください。レイアウト シフトの時間は、[Experience] ペインで赤いレイアウト シフトの長方形の長さを確認して概算することもできます。

レイアウト シフトの DevTools の [Event Log] タブのスクリーンショット

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

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

レイアウト シフト領域をハイライト表示すると、ページ上で発生するレイアウト シフトの位置とタイミングを一目で把握できます。

DevTools で Layout Shift Regions を有効にするには、[Settings] > [More Tools] > [Rendering] > [Layout Shift Regions] に移動し、デバッグするページを更新します。レイアウト シフトの領域は一時的に紫色でハイライト表示されます。

レイアウト シフトの原因を特定するための思考プロセス

以下の手順を使用すると、レイアウト シフトがいつ、どのように発生するかに関係なく、レイアウト シフトの原因を特定できます。これらの手順は、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 アニメーションを作成する方法をご覧ください。

レイアウト シフトの再現

再現できないレイアウト シフトは修正できません。サイトのレイアウトの安定性をより正確に把握するための最も簡単で効果的な方法の 1 つは、レイアウト シフトをトリガーすることを目的としてサイトを操作するのに 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 を任意のフロントエンド ロギングツールと組み合わせて使用し、問題に関する詳細情報を収集します。詳しくは、ページ内で最もシフトした要素をトラッキングするコードのサンプルをご覧ください。