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

ウェブサイトのフィールドデータで遅いインタラクションを見つける方法を学び、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 を入力すると、INP を含む複数の指標について、その URL のフィールドデータ(利用可能な場合)が表示されます。切り替えボタンを使用して、モバイルとパソコンのディメンションの 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 からのエントリなど)が表示されます。
ウェブ バイタル ライブラリのデータがコンソールに表示される仕組み。

アトリビューション ビルドでは、ページの 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 撮影)。