現場でパフォーマンスをデバッグ

パフォーマンス データにデバッグ情報を関連付ける方法を学び、分析結果に基づいて実際のユーザーの問題を特定して修正できるようにします。

Google では、パフォーマンスを測定してデバッグするためのツールとして、次の 2 つのカテゴリを提供しています。

  • ラボツール: Lighthouse などのツール。さまざまな状況を模倣したシミュレーション環境(低速なネットワークやローエンドのモバイル デバイスなど)でページを読み込みます。
  • フィールド ツール: Chrome ユーザー エクスペリエンス レポート(CrUX)などのツール。Chrome の実際のユーザーデータの集計に基づいています。(PageSpeed InsightsSearch Console などのツールによって報告されるフィールド データは、CrUX データに基づいています)。

現場のツールはより正確なデータ(実際のユーザーが実際に体験しているデータ)を提供しますが、多くの場合、ラボのツールは問題の特定と修正に役立ちます。

CrUX データはページの実際のパフォーマンスをより正確に反映したもので、CrUX スコアがパフォーマンスの改善に役立つとは限りません。

一方、Lighthouse では問題を特定し、具体的な改善案を提示します。ただし、Lighthouse では、ページの読み込み時に検出されたパフォーマンスの問題に対してのみ提案が行われます。ページ上でのスクロールやボタンのクリックなどのユーザー操作の結果としてのみ現れる問題は検出されません。

ここで重要な疑問が生じます。「Core Web Vitals やその他のパフォーマンス指標のデバッグ情報を、実際のユーザーからどのように取得できるのか?」

この投稿では、現在の Core Web Vitals の各指標について追加のデバッグ情報を収集するために使用できる API と、既存の分析ツールでそのデータをキャプチャする方法について、詳しく説明します。

アトリビューションとデバッグ用の API

CLS

Core Web Vitals のすべての指標の中で、CLS はおそらく、現場でのデバッグ情報を収集することが最も重要な指標です。CLS はページの存続期間全体で測定されるため、ユーザーによるページの操作方法(スクロール距離、クリックする内容など)は、レイアウト シフトの有無やシフトする要素に大きな影響を与える可能性があります。

PageSpeed Insights の次のレポートについて考えてみましょう。

さまざまな CLS 値が示された PageSpeed Insights レポート

ラボの CLS(Lighthouse)とフィールドの CLS(CrUX データ)でレポートされる値は大きく異なります。これは、Lighthouse でのテスト時に使用されていないインタラクティブなコンテンツがページに大量に含まれていることを考慮すると理にかなっています。

ただし、ユーザー操作がフィールド データに影響することを理解している場合でも、75 パーセンタイルでスコアが 0.3 になるように、ページ上のどの要素がシフトしているのかを把握する必要があります。

これは、LayoutShiftAttribution インターフェースによって可能になります。

レイアウト シフトのアトリビューションを取得する

LayoutShiftAttribution インターフェースは、Layout Instability API が出力する各 layout-shift エントリで公開されます。

両方のインターフェースの詳細については、レイアウト シフトをデバッグするをご覧ください。この投稿で知っておくべき主なことは、デベロッパーはページ上で発生するすべてのレイアウト シフトと、どの要素がシフトしているのかを観察できるということです。

次に、各レイアウト シフトとシフトされた要素をログに記録するコード例を示します。

new PerformanceObserver((list) => {
  for (const {value, startTime, sources} of list.getEntries()) {
    // Log the shift amount and other entry info.
    console.log('Layout shift:', {value, startTime});
    if (sources) {
      for (const {node, curRect, prevRect} of sources) {
        // Log the elements that shifted.
        console.log('  Shift source:', node, {curRect, prevRect});
      }
    }
  }
}).observe({type: 'layout-shift', buffered: true});

発生するレイアウト シフトごとにデータを測定して分析ツールに送信するのは現実的ではありませんが、すべてのシフトをモニタリングすることで、最悪のシフトを追跡し、それらに関する情報のみをレポートできます。

目標は、すべてのユーザーで発生するすべてのレイアウト シフトを特定して修正することではありません。その目的は、最も多くのユーザー数に影響し、ページの CLS に特に大きな影響を与えるシフトを 75 パーセンタイルで特定することです。

また、シフトが発生するたびに最大のソース要素を計算する必要はありません。分析ツールに CLS 値を送信する準備ができた場合にのみ、ソース要素を計算する必要があります。

次のコードは、CLS に影響した layout-shift エントリのリストを受け取り、最大のシフトから最大のソース要素を返します。

function getCLSDebugTarget(entries) {
  const largestEntry = entries.reduce((a, b) => {
    return a && a.value > b.value ? a : b;
  });
  if (largestEntry && largestEntry.sources && largestEntry.sources.length) {
    const largestSource = largestEntry.sources.reduce((a, b) => {
      return a.node && a.previousRect.width * a.previousRect.height >
          b.previousRect.width * b.previousRect.height ? a : b;
    });
    if (largestSource) {
      return largestSource.node;
    }
  }
}

大きな変化に寄与している最大の要素を特定したら、それを分析ツールに報告できます。

特定のページの CLS に最も寄与する要素はユーザーによって異なりますが、すべてのユーザーからこれらの要素を集約すると、最も多くのユーザーに影響を与えるシフト要素のリストを生成できます。

これらの要素のシフトの根本原因を特定して修正すると、分析コードでは、小さなシフトがページの「最悪」のシフトとして報告されるようになります。最終的には、ページの「良好」なしきい値である 0.1 の範囲内に収まる程度に、報告される変化はすべて小さくなります。

他にも、最大のシフトソース要素とともにキャプチャすると役立つ可能性のあるメタデータは次のとおりです。

  • 最大シフトの時間
  • 最大シフト時の URL パス(シングルページ アプリケーションなど、URL を動的に更新するサイトの場合)。

LCP

フィールド内の LCP をデバッグするために必要な主な情報は、その特定のページ読み込みで最大の要素(LCP 候補要素)であった特定の要素です。

まったく同じページであっても、ユーザーごとに LCP 候補要素が異なることはよくあります。

この問題は次のような原因で発生することがあります。

  • ユーザー デバイスの画面解像度はさまざまであるため、ページ レイアウトも異なるため、ビューポート内に表示される要素も異なります。
  • ユーザーが常にページの最上部までスクロールしたとは限りません。多くの場合、リンクにはフラグメント識別子テキスト フラグメントが含まれています。つまり、ページのどのスクロール位置でもページが読み込まれ、表示される可能性があります。
  • コンテンツは現在のユーザーに合わせてパーソナライズされている可能性があるため、LCP 候補要素はユーザーごとに大きく異なる可能性があります。

つまり、特定のページで最も一般的な LCP 候補要素となる要素や要素のセットを推測することはできません。実際のユーザー行動に基づいて測定する必要があります。

LCP 候補要素を特定する

JavaScript で LCP 候補要素を決定するには、Largest Contentful Paint API を使用できます。この API は、LCP 時間の値の特定に使用する API と同じです。

largest-contentful-paint エントリを確認する場合、最後のエントリの element プロパティを確認することで、現在の LCP 候補要素を特定できます。

new PerformanceObserver((list) => {
  const entries = list.getEntries();
  const lastEntry = entries[entries.length - 1];

  console.log('LCP element:', lastEntry.element);
}).observe({type: 'largest-contentful-paint', buffered: true});

LCP 候補要素がわかったら、指標値とともに分析ツールに送信できます。これにより、CLS と同様に、最初に最適化すべき要素を特定できます。

LCP 候補要素に加えて、LCP サブパート時間を測定すると役に立つ場合があります。これは、サイトに関連する具体的な最適化手順を判断する際に役立ちます。

FID

フィールドの FID をデバッグする場合、FID は初回入力イベントのレイテンシ全体の遅延部分のみを測定することに注意することが重要です。つまり、ユーザーが何を操作したかは、ユーザーが操作したときにメインスレッドで他に何が起こっていたかほど重要ではありません。

たとえば、サーバーサイド レンダリング(SSR)をサポートする多くの JavaScript アプリケーションは、ユーザー入力に対してインタラクティブになる前、つまりコンテンツをインタラクティブにするために必要な JavaScript の読み込みが完了する前に、画面にレンダリングできる静的 HTML を配信します。

このような種類のアプリケーションでは、最初の入力がハイドレーションの前か後に発生したかを知ることが非常に重要です。ハイドレーションが完了する前に多くのユーザーがページを操作しようとしていることが判明した場合は、インタラクティブに見える状態ではなく、無効状態または読み込み状態でページをレンダリングすることを検討してください。

アプリケーション フレームワークがハイドレーション タイムスタンプを公開している場合、それを first-input エントリのタイムスタンプと比較して、最初の入力がハイドレーションの前か後かを判断できます。フレームワークがそのタイムスタンプを公開しない場合や、ハイドレーションをまったく使用しない場合、もう 1 つの有用なシグナルとして、入力が発生したのが JavaScript の読み込み完了前か後かがあります。

DOMContentLoaded イベントは、ページの HTML の読み込みと解析が完了すると発生します。これには、同期スクリプト、遅延スクリプト、モジュール スクリプト(静的にインポートされたすべてのモジュールを含む)の読み込みの待機も含まれます。そのため、そのイベントのタイミングを使用して、FID が発生したタイミングと比較できます。

次のコードは、first-input エントリを監視し、DOMContentLoaded イベントの終了前に最初の入力が発生したかどうかをログに記録します。

new PerformanceObserver((list) => {
  const fidEntry = list.getEntries()[0];
  const navEntry = performance.getEntriesByType('navigation')[0];
  const wasFIDBeforeDCL =
    fidEntry.startTime < navEntry.domContentLoadedEventStart;

  console.log('FID occurred before DOMContentLoaded:', wasFIDBeforeDCL);
}).observe({type: 'first-input', buffered: true});

FID ターゲット要素とイベントタイプを特定する

その他の有用なデバッグ シグナルは、操作された要素と操作のタイプ(mousedownkeydownpointerdown など)です。要素自体のインタラクションは FID には寄与しません(FID はイベントの合計レイテンシの遅延部分です)。ユーザーがどの要素を操作しているかを把握することは、FID を改善する最適な方法を決定するのに役立ちます。

たとえば、ユーザーの最初の操作の大部分が特定の要素に関するものである場合は、その要素に必要な JavaScript コードを HTML にインライン化し、残りは遅延読み込みすることを検討してください。

最初の入力イベントに関連付けられたインタラクション タイプと要素を取得するには、first-input エントリの target プロパティと name プロパティを参照します。

new PerformanceObserver((list) => {
  const fidEntry = list.getEntries()[0];

  console.log('FID target element:', fidEntry.target);
  console.log('FID interaction type:', fidEntry.name);
}).observe({type: 'first-input', buffered: true});

INP

INP は FID と非常によく似ており、フィールドでキャプチャする最も有用な情報は次のとおりです。

  1. 操作された要素
  2. やり取りの種類
  3. そのやり取りが行われた日時

FID と同様に、インタラクションが遅い主な原因は、JavaScript の読み込み中にメインスレッドがブロックされることです。ページの読み込み中に遅いインタラクションが発生しているかどうかがわかれば、問題を解決するために何を行う必要があるかを判断しやすくなります。

FID とは異なり、INP 指標はインタラクションの完全なレイテンシを考慮します。これには、登録されたイベント リスナーの実行に要する時間と、すべてのイベント リスナーが実行された後に次のフレームを描画するのに要する時間が含まれます。つまり、INP では、インタラクションを遅くする傾向があるターゲット要素と、それらがどのような種類のインタラクションであるかを知ることがさらに有益です。

INP と FID はどちらも Event Timing API に基づいているため、JavaScript でこの情報を確認する方法は前の例と非常によく似ています。次のコードは、INP エントリのターゲット要素と時間(DOMContentLoaded を基準)をログに記録します。

function logINPDebugInfo(inpEntry) {
  console.log('INP target element:', inpEntry.target);
  console.log('INP interaction type:', inpEntry.name);

  const navEntry = performance.getEntriesByType('navigation')[0];
  const wasINPBeforeDCL =
    inpEntry.startTime < navEntry.domContentLoadedEventStart;

  console.log('INP occurred before DCL:', wasINPBeforeDCL);
}

なお、このコードでは、どの event エントリが INP エントリであるかを判断する方法は示されていません。そのロジックが複雑であるためです。次のセクションでは、web-vitals JavaScript ライブラリを使用してこの情報を取得する方法について説明します。

web-vitals JavaScript ライブラリの使用

上記のセクションでは、分析ツールに送信するデータに含めるデバッグ情報をキャプチャするための一般的な推奨事項とコードサンプルを示しています。

バージョン 3 以降、web-vitals JavaScript ライブラリには、これらの情報をすべて表示するアトリビューション ビルドと、いくつかの追加のシグナルが含まれています。

次のサンプルコードは、パフォーマンスの問題の根本原因の特定に役立つデバッグ文字列を含む、追加のイベント パラメータ(またはカスタム ディメンション)を設定する方法を示しています。

import {onCLS, onFID, onINP, onLCP} from 'web-vitals/attribution';

function sendToGoogleAnalytics({name, value, id, attribution}) {
  const eventParams = {
    metric_value: value,
    metric_id: id,
  }

  switch (name) {
    case 'CLS':
      eventParams.debug_target = attribution.largestShiftTarget;
      break;
    case 'LCP':
      eventParams.debug_target = attribution.element;
      break;
    case 'FID':
    case 'INP':
      eventParams.debug_target = attribution.eventTarget;
      break;
  }

  // Assumes the global `gtag()` function exists, see:
  // https://developers.google.com/analytics/devguides/collection/ga4
  gtag('event', name, eventParams);
}

onCLS(sendToGoogleAnalytics);
onLCP(sendToGoogleAnalytics);
onFID(sendToGoogleAnalytics);
onINP(sendToGoogleAnalytics);

このコードは Google アナリティクスに固有のものですが、一般的な考え方は他の分析ツールにも応用できます。

このコードは、1 つのデバッグ シグナルについてレポートする方法も示しているだけですが、指標ごとに複数の異なるシグナルを収集してレポートできると便利な場合があります。たとえば、INP をデバッグするために、インタラクションのタイプ、時間、操作対象の要素を収集できます。次の例に示すように、web-vitals アトリビューション ビルドはこれらの情報をすべて公開します。

import {onCLS, onFID, onINP, onLCP} from 'web-vitals/attribution';

function sendToGoogleAnalytics({name, value, id, attribution}) {
  const eventParams = {
    metric_value: value,
    metric_id: id,
  }

  switch (name) {
    case 'INP':
      eventParams.debug_target = attribution.eventTarget;
      eventParams.debug_type = attribution.eventType;
      eventParams.debug_time = attribution.eventTime;
      eventParams.debug_load_state = attribution.loadState;
      break;

    // Additional metric logic...
  }

  // Assumes the global `gtag()` function exists, see:
  // https://developers.google.com/analytics/devguides/collection/ga4
  gtag('event', name, eventParams);
}

onCLS(sendToGoogleAnalytics);
onLCP(sendToGoogleAnalytics);
onFID(sendToGoogleAnalytics);
onINP(sendToGoogleAnalytics);

公開されているデバッグ シグナルの完全なリストについては、web-vitals アトリビューションのドキュメントをご覧ください。

データの報告と可視化

デバッグ情報と指標値の収集を開始したら、次のステップとして、すべてのユーザーのデータを集計して、パターンと傾向を探します。

前述のように、ユーザーが直面しているすべての問題に必ずしも対処する必要はありません。特にまずは、最も多くのユーザーに影響する問題(ウェブに関する主な指標のスコアに悪影響を与える問題)に対処する必要があります。

GA4 については、BigQuery を使用してデータのクエリと可視化を行う方法に関する専用記事をご覧ください。

まとめ

この投稿で、既存のパフォーマンス API と web-vitals ライブラリを使用してデバッグ情報を取得し、実際のユーザーによるアクセスに基づいてパフォーマンスを診断する具体的な方法の概要がお役に立てば幸いです。このガイドでは Core Web Vitals を中心に説明していますが、JavaScript で測定可能なパフォーマンス指標のデバッグにもこのコンセプトが適用されます。

パフォーマンスの測定を始めたばかりで、すでに Google アナリティクスをご利用の場合は、ウェブに関する主な指標のデバッグ情報の報告をすでにサポートしているため、ウェブに関する指標レポートツールから始めることをおすすめします。

アナリティクス ベンダーとして、サービスを改善し、より多くのデバッグ情報をユーザーに提供したいとお考えの場合は、ここで説明する手法のいくつかを検討してください。ここで紹介するアイデアにとらわれる必要はありません。この投稿は、すべての分析ツールに一般的に適用できることを目的としていますが、個々の分析ツールでは、さらに多くのデバッグ情報をキャプチャして報告できる(および報告するべき)可能性があります。

API 自体の機能または情報が不足しているため、これらの指標をデバッグできないと思われる場合は、web-vitals-feedback@googlegroups.com にフィードバックをお送りください。