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

パフォーマンス データにデバッグ情報を関連付ける方法を 解説し、アナリティクスで実際に発生する問題を特定して修正する

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});

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

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

また、シフトが発生するたびに最大のソース要素を計算する必要はありません。必要なのは、分析ツールに 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 を使用できます。これは、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 エントリのタイムスタンプと比較することで、最初の入力がハイドレーションの前か後かを判断できます。フレームワークがそのタイムスタンプを公開しない場合や、ハイドレーションをまったく使用しない場合、もう一つの有用なシグナルとして、入力が 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 エントリであるかを判断する方法は示されていません(ロジックの方が複雑なため、このコードでは 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 アナリティクス固有のものですが、一般的な考え方は他の分析ツールにも応用できます。

このコードは、単一のデバッグ シグナルについてレポートする方法も示しているだけですが、指標ごとに複数の異なるシグナルを収集してレポートできると便利な場合があります。たとえば、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 にフィードバックをお送りください。