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

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

フィールド データは、実際のユーザーがウェブサイトをどのように利用しているかを示すデータです。ラボデータだけでは見つけられない問題を特定できます。Interaction to Next Paint(INP)に関しては、フィールド データは遅いインタラクションを特定するうえで不可欠であり、それらを修正するうえで重要な手がかりとなります。

このガイドでは、Chrome ユーザー エクスペリエンス レポート(CrUX)のフィールド データを使用してウェブサイトの INP をすばやく評価し、ウェブサイトの INP に問題があるかどうかを確認する方法について説明します。次に、web-vitals 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 つのウェブに関する主な指標(LCP、INP、CLS)と、診断指標(TTFB、FCP)、非推奨のウェブに関する主な指標(FID)が表示されています。
PageSpeed Insights に表示される CrUX データの読み取り。この例では、指定されたウェブページの INP に改善が必要です。

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

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

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

Browser Support

  • Chrome: 96.
  • Edge: 96.
  • Firefox Technology Preview: supported.
  • Safari: not supported.

Source

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 ライブラリのアトリビューション ビルドをさらに活用する

ウェブバイタル ライブラリのアトリビューション ビルドでは、フィールドのユーザーから取得できる追加のデータが提供されます。このデータは、ウェブサイトの 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 のしきい値の範囲内(良好)にあること、および 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)

Browser Support

  • Chrome: 123.
  • Edge: 123.
  • Firefox: not supported.
  • Safari: not supported.

Source

フィールドデータを使用してインタラクションをデバッグするのは難しい作業です。しかし、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.toSorted((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.toSorted((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.toSorted((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.toSorted((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.toSorted((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 プロバイダ)などのフィールド データ収集ツールを使用すると、どのインタラクションが最も問題があるかをより確信を持って特定し、ラボで問題のあるインタラクションを再現して修正に進むことができます。

ヒーロー画像は UnsplashFederico Respini 氏によるものです。