現場でゆっくりとしたやり取りを見つける

ウェブサイトのフィールドデータで遅いインタラクションを見つける方法を学び、Interaction to Next Paint を改善する機会を見つけましょう。

フィールド データは、ウェブサイトの実際のユーザー エクスペリエンスを示すデータです。ラボデータだけでは見つけられない問題を特定できます。Interaction to Next Paint(INP)の場合、フィールドデータはインタラクションの遅延を特定するために不可欠であり、問題の解決に役立つ重要な手がかりを提供します。

このガイドでは、Chrome ユーザー エクスペリエンス レポート(CrUX)のフィールド データを使用してウェブサイトの INP を迅速に評価し、ウェブサイトに INP に関する問題があるかどうかを確認する方法を説明します。次に、ウェブバイタルの JavaScript ライブラリのアトリビューション ビルドと、Long Animation Frames API(LoAF) から得られる新しい分析情報を使用することで、ウェブサイトの遅いインタラクションに関するフィールドデータを収集して解釈する方法を学びます。

CrUX でウェブサイトの INP を評価する

ウェブサイトのユーザーからフィールド データを収集していない場合、CrUX から始めるとよいでしょう。CrUX は、テレメトリー データの送信を有効にした実際の Chrome ユーザーからフィールド データを収集します。

CrUX データはさまざまな領域に表示されますが、それは探している情報の範囲によって異なります。CrUX では、次の項目について INP やその他の Core Web Vitals に関するデータを提供できます。

  • 個々のページとオリジン全体を PageSpeed Insights で確認する。
  • ページの種類。たとえば、多くの e コマース ウェブサイトには、商品詳細ページと商品リスティング ページの種類があります。一意のページタイプの CrUX データは Search Console で取得できます。

まず、PageSpeed Insights にウェブサイトの URL を入力します。URL を入力すると、その URL のフィールド データが、INP を含む複数の指標について表示されます(利用可能な場合)。切り替えボタンを使用して、モバイルとパソコンのディメンションの INP 値を確認することもできます。

PageSpeed Insights の CrUX で表示されるフィールド データ。3 つの Core Web Vitals の LCP、INP、CLS、診断指標として TTFB、FCP、非推奨の Core Web Vitals 指標として FID が表示されます。
PageSpeed Insights に表示される CrUX データのレポート。この例では、指定されたウェブページの INP に改善が必要です。

このデータは、問題が発生しているかどうかを判断するのに役立ちます。ただし、CrUX では問題の原因を特定することはできません。ウェブサイトのユーザーから独自のフィールドデータを収集して、この質問に答えることができるリアルユーザー モニタリング(RUM)ソリューションは数多くあります。その 1 つが、web-vitals JavaScript ライブラリを使用してフィールドデータを自分で収集する方法です。

web-vitals JavaScript ライブラリを使用してフィールドデータを収集する

web-vitals JavaScript ライブラリは、ウェブサイトに読み込んで、ウェブサイトのユーザーからフィールド データを収集できるスクリプトです。このレポートは、サポートしているブラウザの INP など、さまざまな指標を記録するために使用できます。

対応ブラウザ

  • Chrome: 96。
  • Edge: 96。
  • Firefox: サポートされていません。
  • Safari: サポートされていません。

ソース

web-vitals ライブラリの標準ビルドを使用すると、現場のユーザーから基本的な INP データを取得できます。

import {onINP} from 'web-vitals';

onINP(({name, value, rating}) => {
  console.log(name);    // 'INP'
  console.log(value);   // 512
  console.log(rating);  // 'poor'
});

ユーザーから送信されたフィールドデータを分析するには、このデータをどこかに送信する必要があります。

import {onINP} from 'web-vitals';

onINP(({name, value, rating}) => {
  // Prepare JSON to be sent for collection. Note that
  // you can add anything else you'd want to collect here:
  const body = JSON.stringify({name, value, rating});

  // Use `sendBeacon` to send data to an analytics endpoint.
  // For Google Analytics, see https://github.com/GoogleChrome/web-vitals#send-the-results-to-google-analytics.
  navigator.sendBeacon('/analytics', body);
});

ただし、このデータだけでは CrUX で得られる情報以上の情報を得ることはできません。そこで役立つのが、アトリビューション ビルドの web-vitals ライブラリです。

web-vitals ライブラリのアトリビューション ビルドをさらに活用する

web-vitals ライブラリのアトリビューション ビルドでは、現場のユーザーから入手できる追加データが表示されるため、ウェブサイトの INP に影響を与えている問題のあるインタラクションをより適切にトラブルシューティングできます。このデータには、ライブラリの onINP() メソッドで表示される attribution オブジェクトからアクセスできます。

import {onINP} from 'web-vitals/attribution';

onINP(({name, value, rating, attribution}) => {
  console.log(name);         // 'INP'
  console.log(value);        // 56
  console.log(rating);       // 'good'
  console.log(attribution);  // Attribution data object
});
web-vitals ライブラリのコンソール ログの表示方法。この例のコンソールには、指標の名前(INP)、INP 値(56)、その値が INP しきい値内にあること(良好)、アトリビューション オブジェクトに表示されるさまざまな情報(The Long Animation Frames API からのエントリなど)が表示されます。
web-vitals ライブラリのデータがコンソールにどのように表示されるか。

アトリビューション ビルドでは、ページの INP 自体に加えて、インタラクションの遅延の原因を把握するために役立つ多くのデータが提供されます。たとえば、インタラクションのどの部分に重点を置くべきかを確認できます。次のような重要な情報を得るために役立ちます。

  • 「ユーザーは読み込み中にページを操作しましたか?」
  • 「操作のイベント ハンドラは長時間実行されましたか?」
  • 「インタラクション イベント ハンドラ コードの開始が遅れたか?発生した場合、その時点でメインスレッドで他に何が行われていましたか?」
  • 「インタラクションによってレンダリング処理が大量に発生し、次のフレームのペイントが遅れたか?」

次の表に、ライブラリから取得できる基本的なアトリビューション データの一部を示します。これらのデータは、ウェブサイトのインタラクションの遅延の概要を把握するのに役立ちます。

attribution オブジェクトキー データ
interactionTarget ページの INP 値を生成する要素を指す CSS セレクタ(例: button#save)。
interactionType インタラクションの種類(クリック、タップ、キーボード入力)。
inputDelay* インタラクションの入力遅延
processingDuration* ユーザー操作に対する最初のイベント リスナーの実行を開始してから、すべてのイベント リスナーの処理が完了するまでの時間。
presentationDelay* イベント ハンドラが終了してから次のフレームがペイントされるまでに発生する、インタラクションのプレゼンテーション遅延
longAnimationFrameEntries* やり取りに関連付けられた LoAF のエントリ。詳細については、次をご覧ください。
*バージョン 4 の新機能

web-vitals ライブラリのバージョン 4 以降では、INP フェーズの内訳(入力遅延、処理時間、表示遅延)と Long Animation Frames API(LoAF)で提供されるデータを使用して、問題のあるインタラクションについてさらに詳細な分析情報を得ることができます。

Long Animation Frames API(LoAF)

対応ブラウザ

  • Chrome: 123。
  • Edge: 123。
  • Firefox: サポートされていません。
  • Safari: サポートされていません。

ソース

フィールドデータを使用してインタラクションをデバッグするのは難しい作業です。LoAF のデータを使用すると、インタラクションの遅延の原因をより詳しく把握できます。LoAF では、正確な原因を特定するために使用できる詳細なタイミングなどのデータが多数公開されます。さらに重要なのは、ウェブサイトのコード内で問題の原因がどこにあるかを確認できることです。

web-vitals ライブラリのアトリビューション ビルドは、attribution オブジェクトの longAnimationFrameEntries キーの下に LoAF エントリの配列を公開します。次の表に、各 LoAF エントリに含まれる主なデータを示します。

LoAF エントリ オブジェクト キー データ
duration 長いアニメーション フレームの長さ(レイアウトが完了するまでの時間。ペイントとコンポジットは除く)。
blockingDuration タスクの長さによりブラウザが迅速に応答できなかったフレーム内の合計時間。このブロック時間には、JavaScript を実行する長いタスクや、フレーム内の長いレンダリング タスクが含まれる場合があります。
firstUIEventTimestamp フレーム中にイベントがキューに追加されたときのタイムスタンプ。インタラクションの入力遅延の開始を把握するのに役立ちます。
startTime フレームの開始タイムスタンプ。
renderStart フレームのレンダリング処理が開始されたとき。これには、requestAnimationFrame コールバック(該当する場合は ResizeObserver コールバック)が含まれますが、スタイルやレイアウトの処理が開始される前になることもあります。
styleAndLayoutStart フレーム内のスタイル / レイアウト処理が発生したとき。他の利用可能なタイムスタンプを考慮して、スタイルやレイアウトの作業時間を把握する場合に役立ちます。
scripts ページの INP に貢献したスクリプトのアトリビューション情報を含むアイテムの配列。
LoAF モデルに従った長いアニメーション フレームの可視化。
LoAF API に従った長いアニメーション フレームのタイミングの図(blockingDuration を除く)。

これらの情報はすべて、インタラクションの遅延の原因について多くのことを教えてくれますが、LoAF エントリに表示される scripts 配列は特に重要です。

スクリプト アトリビューション オブジェクト キー データ
invoker 起動元。これは、次の行で説明する呼び出し元のタイプによって異なります。起動元の例としては、'IMG#id.onload''Window.requestAnimationFrame''Response.json.then' などの値が挙げられます。
invokerType 呼び出し元のタイプ。'user-callback''event-listener''resolve-promise''reject-promise''classic-script''module-script' のいずれかです。
sourceURL 長いアニメーション フレームの元となったスクリプトの URL。
sourceCharPosition sourceURL で識別されるスクリプトの文字位置。
sourceFunctionName 識別されたスクリプト内の関数の名前。

この配列の各エントリには、このテーブルに示されているデータが含まれています。これにより、遅いインタラクションの原因となったスクリプトとその原因に関する情報が得られます。

操作が遅い一般的な原因を測定して特定する

この情報の活用方法を理解していただくために、このガイドでは、web-vitals ライブラリに表示される LoAF データを使用して、操作が遅い背後にある原因を特定する方法について説明します。

処理に時間がかかる

インタラクションの処理時間とは、インタラクションの登録済みイベント ハンドラ コールバックが完了するまでにかかる時間と、その間に発生する可能性のあるその他の時間のことです。処理時間が長い場合は、web-vitals ライブラリで検出されます。

import {onINP} from 'web-vitals/attribution';

onINP(({name, value, attribution}) => {
  const {processingDuration} = attribution; // 512.5
});

インタラクションが遅くなる主な原因は、イベント ハンドラ コードの実行に時間がかかりすぎたことにあると考えるのは自然なことですが、必ずしもそうとは限りません。これが問題であることを確認したら、LoAF データを使用して詳細を把握できます。

import {onINP} from 'web-vitals/attribution';

onINP(({name, value, attribution}) => {
  const {processingDuration} = attribution; // 512.5

  // Get the longest script from LoAF covering `processingDuration`:
  const loaf = attribution.longAnimationFrameEntries.at(-1);
  const script = loaf?.scripts.sort((a, b) => b.duration - a.duration)[0];

  if (script) {
    // Get attribution for the long-running event handler:
    const {invokerType} = script;        // 'event-listener'
    const {invoker} = script;            // 'BUTTON#update.onclick'
    const {sourceURL} = script;          // 'https://example.com/app.js'
    const {sourceCharPosition} = script; // 83
    const {sourceFunctionName} = script; // 'update'
  }
});

上記のコード スニペットからわかるように、LoAF データを使用して、処理時間の長いインタラクションの背後にある正確な原因を特定できます。たとえば、次のような原因があります。

  • 要素と、その要素に登録されているイベント リスナー。
  • 長時間実行イベント ハンドラ コードを含むスクリプト ファイルと、その中の文字位置。
  • 関数名。

この種のデータは非常に貴重です。処理時間の長い値の原因となったインタラクション(またはそのイベント ハンドラ)を正確に調べる必要はもうありません。また、サードパーティ スクリプトは独自のイベント ハンドラを登録することが多いので、問題の原因が自分のコードかどうかを判断できます。自分で制御できるコードについては、時間のかかるタスクの最適化を検討する必要があります。

入力遅延が長い

長時間実行されるイベント ハンドラは一般的ですが、他にも考慮すべきインタラクションの部分があります。1 つは処理時間の前に発生し、入力遅延と呼ばれます。これは、ユーザーが操作を開始してから、イベント ハンドラ コールバックの実行が開始されるまでの時間です。これは、メインスレッドがすでに別のタスクを処理している場合に発生します。web-vitals ライブラリのアトリビューション ビルドでは、インタラクションの入力遅延の長さを把握できます。

import {onINP} from 'web-vitals/attribution';

onINP(({name, value, attribution}) => {
  const {inputDelay} = attribution; // 125.59439536
});

一部のインタラクションの入力遅延が長い場合は、インタラクションの時点でページで何が起こっていたかを確認する必要があります。入力遅延の原因は、ページの読み込み中か、読み込み後に発生したかによって異なります。

ページの読み込み中ですか?

多くの場合、ページの読み込み中はメインスレッドが最も負荷が高くなります。この間に、あらゆる種類のタスクがキューに追加され、処理されています。これらの処理中にユーザーがページを操作しようとすると、操作が遅れる可能性があります。JavaScript を大量に読み込むページでは、スクリプトのコンパイルや評価、ユーザー操作に備えてページを準備する関数の実行などの処理が開始されることがあります。この仕組みは、このようなアクティビティの発生中にユーザーがたまたま操作した場合、その妨げとなるため、ウェブサイトのユーザーもこれに該当するかどうかを確認できます。

import {onINP} from 'web-vitals/attribution';

onINP(({name, value, attribution}) => {
  const {inputDelay} = attribution; // 125.59439536

  // Get the longest script from the first LoAF entry:
  const loaf = attribution.longAnimationFrameEntries[0];
  const script = loaf?.scripts.sort((a, b) => b.duration - a.duration)[0];

  if (script) {
    // Invoker types can describe if script eval blocked the main thread:
    const {invokerType} = script;    // 'classic-script' | 'module-script'
    const {sourceLocation} = script; // 'https://example.com/app.js'
  }
});

フィールドでこのデータを記録し、入力遅延が長く、呼び出し元のタイプが 'classic-script' または 'module-script' である場合は、サイト上のスクリプトの評価に時間がかかっているため、メインスレッドがブロックされ、操作が遅延していると考えられます。このブロック時間を短縮するには、スクリプトを小さなバンドルに分割し、最初は使用しないコードを後で読み込むように遅らせ、サイトを監査して、完全に削除できる未使用のコードを探します。

ページの読み込み後ですか?

入力の遅延はページの読み込み中に発生することがよくありますが、まったく別の原因でページの読み込みに発生する可能性もあります。ページの読み込み後に入力が遅れる一般的な原因としては、以前の setInterval 呼び出しが原因で定期的に実行されるコードや、以前に実行するためにキューに追加されたイベント コールバックがまだ処理中である場合などがあります。

import {onINP} from 'web-vitals/attribution';

onINP(({name, value, attribution}) => {
  const {inputDelay} = attribution; // 125.59439536

  // Get the longest script from the first LoAF entry:
  const loaf = attribution.longAnimationFrameEntries[0];
  const script = loaf?.scripts.sort((a, b) => b.duration - a.duration)[0];

  if (script) {
    const {invokerType} = script;        // 'user-callback'
    const {sourceURL} = script;          // 'https://example.com/app.js'
    const {sourceCharPosition} = script; // 83
    const {sourceFunctionName} = script; // 'update'
  }
});

処理時間の長い値のトラブルシューティングと同様に、前述の原因による入力遅延が長い場合は、詳細なスクリプト アトリビューション データが提供されます。ただし、呼び出し元のタイプは、インタラクションの遅延の原因となった作業の性質によって異なります。

  • 'user-callback' は、ブロックするタスクが setIntervalsetTimeout、または requestAnimationFrame からのものであることを示します。
  • 'event-listener' は、ブロッキング タスクが、キューに入れられ、まだ処理中である以前の入力からのものであることを示します。
  • 'resolve-promise''reject-promise' は、ブロック タスクが、先に開始された非同期処理から実行され、ユーザーがページを操作しようとしたときに解決または拒否され、操作が遅れたことを意味します。

いずれにしても、スクリプト属性データを見れば、どこから着手すればよいか、入力遅延の原因が独自のコードによるものか、サードパーティのスクリプトによるものかがわかります。

表示の遅延が長い

表示遅延はインタラクションの最後の部分であり、インタラクションのイベント ハンドラが終了してから次のフレームがペイントされるまでの期間です。インタラクションによるイベント ハンドラ内の処理によってユーザー インターフェースのビジュアル状態が変更された場合に発生します。処理時間や入力遅延と同様に、web-vitals ライブラリでは、インタラクションの表示遅延の長さを把握できます。

import {onINP} from 'web-vitals/attribution';

onINP(({name, value, attribution}) => {
  const {presentationDelay} = attribution; // 113.32307691
});

このデータを記録して、ウェブサイトの INP に貢献するインタラクションの表示遅延が高い場合は、原因はさまざまですが、注意すべき原因がいくつかあります。

スタイルとレイアウトの作業に費用がかかる

プレゼンテーションの遅延が長い場合は、複雑な CSS セレクタやDOM サイズが大きいなど、さまざまな原因で発生するスタイルの再計算レイアウト作業に時間がかかっている可能性があります。この処理にかかる時間は、web-vitals ライブラリに表示される LoAF タイミングで測定できます。

import {onINP} from 'web-vitals/attribution';

onINP(({name, value, attribution}) => {
  const {presentationDelay} = attribution; // 113.32307691

  // Get the longest script from the last LoAF entry:
  const loaf = attribution.longAnimationFrameEntries.at(-1);
  const script = loaf?.scripts.sort((a, b) => b.duration - a.duration)[0];

  // Get necessary timings:
  const {startTime} = loaf; // 2120.5
  const {duration} = loaf;  // 1002

  // Figure out the ending timestamp of the frame (approximate):
  const endTime = startTime + duration; // 3122.5

  // Get the start timestamp of the frame's style/layout work:
  const {styleAndLayoutStart} = loaf; // 3011.17692309

  // Calculate the total style/layout duration:
  const styleLayoutDuration = endTime - styleAndLayoutStart; // 111.32307691

  if (script) {
    // Get attribution for the event handler that triggered
    // the long-running style and layout operation:
    const {invokerType} = script;        // 'event-listener'
    const {invoker} = script;            // 'BUTTON#update.onclick'
    const {sourceURL} = script;          // 'https://example.com/app.js'
    const {sourceCharPosition} = script; // 83
    const {sourceFunctionName} = script; // 'update'
  }
});

LoAF は、フレームのスタイルとレイアウトの作業時間は示しませんが、開始時刻は示します。この開始タイムスタンプを使用すると、LoAF の他のデータを使用して、フレームの終了時刻を決定し、そこからスタイルとレイアウト作業の開始タイムスタンプを差し引くことで、その処理の正確な継続時間を計算できます。

長時間実行の requestAnimationFrame コールバック

プレゼンテーションの遅延が長引く原因として考えられるのは、requestAnimationFrame コールバックで過剰な処理が行われている場合です。このコールバックの内容は、イベント ハンドラの実行が終了した後、スタイルの再計算とレイアウト作業の直前に実行されます。

コールバック内で行われる処理が複雑な場合、完了までにかなりの時間がかかることがあります。requestAnimationFrame で行っている処理が原因で表示遅延の値が高いと思われる場合は、web-vitals ライブラリによって表示される LoAF データを使用して、次のシナリオを特定できます。

onINP(({name, value, attribution}) => {
  const {presentationDelay} = attribution; // 543.1999999880791

  // Get the longest script from the last LoAF entry:
  const loaf = attribution.longAnimationFrameEntries.at(-1);
  const script = loaf?.scripts.sort((a, b) => b.duration - a.duration)[0];

  // Get the render start time and when style and layout began:
  const {renderStart} = loaf;         // 2489
  const {styleAndLayoutStart} = loaf; // 2989.5999999940395

  // Calculate the `requestAnimationFrame` callback's duration:
  const rafDuration = styleAndLayoutStart - renderStart; // 500.59999999403954

  if (script) {
    // Get attribution for the event handler that triggered
    // the long-running requestAnimationFrame callback:
    const {invokerType} = script;        // 'user-callback'
    const {invoker} = script;            // 'FrameRequestCallback'
    const {sourceURL} = script;          // 'https://example.com/app.js'
    const {sourceCharPosition} = script; // 83
    const {sourceFunctionName} = script; // 'update'
  }
});

表示の遅延時間の大部分が requestAnimationFrame コールバックで消費されている場合は、これらのコールバックで行っている処理が、ユーザー インターフェースの実際の更新につながる処理に限定されていることを確認します。DOM に変更を加えたりスタイルを更新したりしない他の作業は、次フレームのペイントが不必要に遅れる原因となるため、注意が必要です。

まとめ

現場データは、現場の実際のユーザーにとって問題となるインタラクションを知るうえで、最も信頼できる情報源です。web-vitals JavaScript ライブラリ(または RUM プロバイダ)などのフィールドデータ収集ツールを使用すると、問題のあるインタラクションの特定をより確実に行うことができます。その後、問題のあるインタラクションをラボで再現し、修正に進むことができます。

Unsplash のヒーロー画像(Federico Respini 撮影)。