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

ウェブサイトのフィールド データで遅いインタラクションを見つけて、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 で表示されるフィールド データ。Core Web Vitals の 3 つの Core Web Vitals に LCP、INP、CLS が表示され、診断指標として TTFB、FCP、サポートが終了した Core Web Vitals 指標として FID が示されます。
PageSpeed Insights に表示される CrUX データのレポート。この例では、指定されたウェブページの INP を改善する必要があります。

このデータは、問題があるかどうかを確認するのに役立ちます。しかし、CrUX が行えないのは、問題の原因となっていることを知ることです。ウェブサイトのユーザーから独自のフィールド データを収集し、その答えを得るのに役立つ Real User Monitoring(RUM)ソリューションは数多くあります。その一つが、web-vitals JavaScript ライブラリを使用してそのフィールド データを自分で収集することです。

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

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

対応ブラウザ

  • 96
  • 96
  • x
  • x

ソース

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 しきい値(良好)内にある INP 値(良好)、Long Animation Frame API のエントリなど、アトリビューション オブジェクトに表示されるさまざまな情報が表示されています。
web-vitals ライブラリのデータをコンソールに表示する方法。

ページの INP 自体に加え、アトリビューション ビルドでは、インタラクションのどの部分に焦点を当てるべきかなど、インタラクションが遅い理由の把握に役立つ多くのデータが提供されます。次のような重要な疑問の解決に役立ちます。

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

次の表に、ライブラリから取得できる基本的なアトリビューション データの一部を示します。これらのデータは、ウェブサイトでのインタラクションが遅い場合のおおまかな原因の特定に役立ちます。

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

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

Long Animation Frame API(LoAF)

対応ブラウザ

  • 123
  • 123
  • x
  • x

ソース

フィールド データを使用したインタラクションのデバッグは困難なタスクです。しかし、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 コールバック

プレゼンテーションの遅延が長引く原因の 1 つとして、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 プロバイダ)などのフィールド データ収集ツールを使用することで、問題が最も大きいインタラクションをより自信を持って判断し、ラボで問題のあるインタラクションを再現して修正を進めることができます。

Federico Respini による Unsplash のヒーロー画像