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

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

Katie Hempenius
Katie Hempenius

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

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 インターフェースは、1 つの DOM 要素の 1 回のシフトを示します。レイアウト シフト中に複数の要素がシフトすると、sources プロパティに複数のエントリが含まれます。

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

// ...
  "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 ミリ秒以内にレイアウト シフトが発生したかどうかを示します。

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

レイアウト シフトの実行時間に関する情報は、[イベントログ] タブで確認できます。 [Experience] ペインで赤色のレイアウト シフトの長方形の長さを確認することで、レイアウト シフトの所要時間を概算することもできます。

レイアウト シフトに関するデベロッパー ツールの [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 アニメーションを作成する方法をご覧ください。

レイアウト シフトの再現

再現できないレイアウトのずれは修正できません。サイトのレイアウトの安定性を把握するための最も簡単で効果的な方法の一つは、レイアウトのずれをトリガーすることを目的として、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 とフロントエンド ロギングツールを併用して、問題に関するより詳細な情報を収集することを検討してください。ページ上で最も大きく移動した要素を追跡する方法のコード例をご覧ください。