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

ウェブサイトのフィールド データから遅いインタラクションを見つけ出し、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 コマース ウェブサイトには、商品の詳細ページと商品リスティング ページのタイプがあります。Search Console で、個別のページタイプに関する CrUX データを取得できます。

まず、PageSpeed Insights にウェブサイトの 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)ソリューションが用意されており、ウェブサイトのユーザーから独自のフィールド データを収集してこれに回答するのに役立ちますが、web-vitals JavaScript ライブラリを使用してフィールド データを自分自身で収集するのも 1 つの方法です。

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);        // 512
  console.log(rating);       // 'poor'
  console.dir(attribution);  // Attribution data
});
web-vitals ライブラリからのコンソールログの表示。この例のコンソールには、指標の名前(INP)、その値が INP しきい値内(良好)内にある INP 値(56)、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 データを使用して、次のような処理時間の値が長いインタラクションの背後にある正確な原因を追跡できます。

  • 要素と、要素に登録済みのイベント リスナー。
  • 長時間実行イベント ハンドラ コードを含むスクリプト ファイル、およびスクリプト ファイル内の文字位置。
  • 関数名。

この種のデータは貴重です。処理時間の値が大きいインタラクション(またはイベント ハンドラのどれか)を正確に突き止める手間を省くことができます。また、サードパーティのスクリプトが独自のイベント ハンドラを登録できることが多いため、自分のコードが原因かどうかを判断できます。自身で管理できるコードについては、時間のかかるタスクの最適化を検討してください。

長い入力遅延

長時間実行されるイベント ハンドラは一般的ですが、インタラクションについて考慮すべき点は他にもあります。処理時間の前に発生する部分の一つを、入力遅延と呼びます。これは、ユーザーが操作を開始してから、イベント ハンドラ コールバックの実行が開始し、メインスレッドがすでに別のタスクを処理しているときに発生するまでの時間です。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' は、ブロックしているタスクが、以前に開始され、ユーザーがページを操作しようとしたときに解決または拒否され、インタラクションが遅延した非同期処理によるものであったことを意味します。

いずれにせよ、スクリプトのアトリビューション データにより、どこから調べればよいか、入力遅延の原因が独自のコードなのか、サードパーティのスクリプトなのかを判断できます。

プレゼンテーションが長時間遅れる

表示の遅延とはインタラクションの最後の 1 マイルであり、インタラクションのイベント ハンドラが終了した時点から始まり、次のフレームがペイントされる時点までです。インタラクションによるイベント ハンドラの作業によってユーザー インターフェースの表示状態が変化した場合に発生します。処理時間や入力遅延と同様に、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 のヒーロー画像。