Next Paint(INP)とのインタラクションの測定

1. はじめに

このインタラクティブな Codelab では、web-vitals ライブラリを使用して Interaction to Next Paint(INP)を測定する方法について説明します。

前提条件

学習内容

  • web-vitals ライブラリをページに追加して、そのアトリビューション データを使用する方法。
  • アトリビューション データを使用して、INP の改善をどこからどのように始めるかを診断します。

必要なもの

  • GitHub からコードのクローンを作成し、npm コマンドを実行できるパソコン。
  • テキスト エディタ。
  • すべてのインタラクション測定が機能する Chrome の最新バージョン。

2. セットアップする

コードを取得して実行する

このコードは web-vitals-codelabs リポジトリにあります。

  1. ターミナルでリポジトリのクローンを作成します(git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git)。
  2. クローン作成したディレクトリ cd web-vitals-codelabs/measuring-inp に移動します。
  3. 依存関係をインストールします: npm ci
  4. ウェブサーバーを起動します。npm run start
  5. ブラウザで http://localhost:8080/ にアクセスします。

ページを試す

この Codelab では、Gastropodicon(人気のカタツムリの解剖学に関する参考サイト)を使用して、INP に関する潜在的な問題を検証します。

Gastropodicon デモページのスクリーンショット

ページを操作して、どの操作が遅いかを確認します。

3. Chrome DevTools の概要

その他のツール > デベロッパー ツール メニューから DevTools を開くか、ページを右クリックして [検証] を選択するか、キーボード ショートカットを使用します。

この Codelab では、[パフォーマンス] パネルと [コンソール] の両方を使用します。これらは、DevTools の上部にあるタブでいつでも切り替えることができます。

  • INP の問題はモバイル デバイスで発生することが多いため、モバイル ディスプレイ エミュレーションに切り替えます
  • デスクトップやノートパソコンでテストする場合、パフォーマンスは実際のモバイル デバイスよりも大幅に向上する可能性があります。パフォーマンスをより現実的に確認するには、[パフォーマンス] パネルの右上にある歯車アイコンをクリックし、[CPU 4 倍の減速] を選択します。

DevTools の [パフォーマンス] パネルとアプリのスクリーンショット。4 倍の CPU スローダウンが選択されている

4. web-vitals のインストール

web-vitals は、ユーザーが体験するウェブに関する主な指標を測定するための JavaScript ライブラリです。このライブラリを使用すると、これらの値をキャプチャして、分析エンドポイントにビーコンを送信し、後で分析できます。この分析の目的は、遅延が発生するタイミングと場所を特定することです。

ライブラリをページに追加する方法はいくつかあります。ライブラリをサイトにインストールする方法は、依存関係の管理方法やビルドプロセスなどの要因によって異なります。すべてのオプションについては、ライブラリのドキュメントをご覧ください。

この Codelab では、特定のビルドプロセスに踏み込まないように、npm からインストールしてスクリプトを直接読み込みます。

使用できる web-vitals には次の 2 つのバージョンがあります。

  • ページ読み込み時のウェブに関する主な指標の指標値をトラッキングする場合は、「標準」ビルドを使用する必要があります。
  • 「attribution」ビルドでは、各指標にデバッグ情報が追加され、指標がその値になる理由を診断できます。

この Codelab で INP を測定するには、アトリビューション ビルドが必要です。

npm install -D web-vitals を実行して、プロジェクトの devDependenciesweb-vitals を追加します。

ページに web-vitals を追加します。

アトリビューション バージョンのスクリプトを index.html の末尾に追加し、結果をコンソールに記録します。

<script type="module">
  import {onINP} from './node_modules/web-vitals/dist/web-vitals.attribution.js';

  onINP(console.log);
</script>

試してみる

コンソールを開いた状態で、ページをもう一度操作してみます。ページ内をクリックしても、何も記録されません。

INP はページのライフサイクル全体で測定されるため、デフォルトでは、ユーザーがページを離れるか閉じるまで web-vitals は INP を報告しません。これは、分析などのビーコン送信に最適な動作ですが、インタラクティブなデバッグにはあまり適していません。

web-vitals には、より詳細なレポート用の reportAllChanges オプションがあります。有効にすると、すべてのインタラクションがレポートされるわけではありませんが、先行するインタラクションよりも遅いインタラクションが発生するたびにレポートされます。

スクリプトにオプションを追加して、ページをもう一度操作してみてください。

<script type="module">
  import {onINP} from './node_modules/web-vitals/dist/web-vitals.attribution.js';

  onINP(console.log, {reportAllChanges: true});
</script>

ページを更新すると、インタラクションがコンソールに報告され、最も遅いインタラクションが更新されるたびに更新されます。たとえば、検索ボックスに入力してから、入力を削除してみてください。

INP メッセージが正常に出力された DevTools コンソールのスクリーンショット

5. アトリビューションの内容

ほとんどのユーザーがページに最初にアクセスしたときに表示される Cookie 同意ダイアログから始めましょう。

多くのページには、ユーザーが Cookie を受け入れたときに Cookie を同期的にトリガーする必要があるスクリプトが含まれており、クリックが遅いインタラクションになります。これが、ここで起こることです。

[はい] をクリックして(デモ)Cookie を受け入れ、DevTools コンソールに記録された INP データを表示します。

DevTools コンソールに記録された INP データ オブジェクト

この最上位の情報は、標準とアトリビューションの両方のウェブ バイタルのビルドで利用できます。

{
  name: 'INP',
  value: 344,
  rating: 'needs-improvement',
  entries: [...],
  id: 'v4-1715732159298-8028729544485',
  navigationType: 'reload',
  attribution: {...},
}

ユーザーがクリックしてから次のペイントまでの時間は 344 ミリ秒で、「改善が必要」な INP でした。entries 配列には、このインタラクションに関連付けられたすべての PerformanceEntry 値(この場合はクリック イベント 1 つのみ)が含まれます。

この期間中に何が起こっているかを確認するには、attribution プロパティが最も重要です。アトリビューション データを構築するために、web-vitals はクリック イベントと重複する Long Animations Frame(LoAF) を特定します。LoAF は、実行されたスクリプトから requestAnimationFrame コールバック、スタイル、レイアウトに費やされた時間まで、そのフレームで費やされた時間に関する詳細なデータを提供できます。

attribution プロパティを展開して詳細情報を表示します。データがはるかに豊富になります。

attribution: {
  interactionTargetElement: Element,
  interactionTarget: '#confirm',
  interactionType: 'pointer',

  inputDelay: 27,
  processingDuration: 295.6,
  presentationDelay: 21.4,

  processedEventEntries: [...],
  longAnimationFrameEntries: [...],
}

まず、操作対象に関する情報があります。

  • interactionTargetElement: 操作された要素へのライブ参照(要素が DOM から削除されていない場合)。
  • interactionTarget: ページ内の要素を見つけるためのセレクタ。

次に、タイミングの概要を示します。

  • inputDelay: ユーザーが操作を開始したとき(マウスをクリックしたときなど)から、その操作のイベント リスナーが実行を開始したときまでの時間。この場合、CPU スロットリングがオンになっていても、入力遅延はわずか 27 ミリ秒程度でした。
  • processingDuration: イベント リスナーが実行を完了するまでの時間。多くの場合、ページには単一のイベント(pointerdownpointerupclick など)に対する複数のリスナーがあります。これらがすべて同じアニメーション フレームで実行される場合、この時間に統合されます。この場合、処理時間は 295.6 ミリ秒で、INP 時間の大部分を占めています。
  • presentationDelay: イベント リスナーが完了してから、ブラウザが次のフレームのペイントを完了するまでの時間。この例では 21.4 ミリ秒です。

これらの INP フェーズは、最適化が必要なものを診断するための重要なシグナルになります。INP の最適化に関するガイドで、このトピックについて詳しく説明しています。

さらに詳しく見てみると、最上位の INP entries 配列の単一イベントとは対照的に、processedEventEntries には 5 つのイベントが含まれています。の機能上の違い

processedEventEntries: [
  {
    name: 'mouseover',
    entryType: 'event',
    startTime: 1801.6,
    duration: 344,
    processingStart: 1825.3,
    processingEnd: 1825.3,
    cancelable: true
  },
  {
    name: 'mousedown',
    entryType: 'event',
    startTime: 1801.6,
    duration: 344,
    processingStart: 1825.3,
    processingEnd: 1825.3,
    cancelable: true
  },
  {name: 'mousedown', ...},
  {name: 'mouseup', ...},
  {name: 'click', ...},
],

最上位のエントリは INP イベント(この場合はクリック)です。アトリビューション processedEventEntries は、同じフレームで処理されたすべてのイベントです。クリック イベントだけでなく、mouseovermousedown などの他のイベントも含まれていることに注意してください。これらの他のイベントも遅かった場合、それらはすべて応答性の低下に寄与しているため、これらのイベントについて知ることは非常に重要です。

最後に、longAnimationFrameEntries 配列があります。これは単一のエントリになる場合もありますが、インタラクションが複数のフレームにまたがる場合もあります。ここでは、単一の長いアニメーション フレームの最も単純なケースを示します。

longAnimationFrameEntries

LoAF エントリを開く:

longAnimationFrameEntries: [{
  name: 'long-animation-frame',
  startTime: 1823,
  duration: 319,

  renderStart: 2139.5,
  styleAndLayoutStart: 2139.7,
  firstUIEventTimestamp: 1801.6,
  blockingDuration: 268,

  scripts: [{...}]
}],

ここでは、スタイリングに費やした時間を把握するなど、さまざまな有用な値を確認できます。Long Animation Frames API の記事では、これらのプロパティについて詳しく説明しています。現時点では、長時間実行されているフレームの原因となったスクリプトの詳細を提供するエントリを含む scripts プロパティに主に注目します。

scripts: [{
  name: 'script',
  invoker: 'BUTTON#confirm.onclick',
  invokerType: 'event-listener',

  startTime: 1828.6,
  executionStart: 1828.6,
  duration: 294,

  sourceURL: 'http://localhost:8080/third-party/cmp.js',
  sourceFunctionName: '',
  sourceCharPosition: 1144
}]

この場合、主に 1 つの event-listener で時間が費やされ、BUTTON#confirm.onclick で呼び出されたことがわかります。スクリプトのソース URL と、関数が定義された文字位置も確認できます。

重要なポイント

このアトリビューション データからこのケースについて何がわかりますか?

  • インタラクションは、button#confirm 要素(attribution.interactionTarget とスクリプト アトリビューション エントリの invoker プロパティから)のクリックによってトリガーされました。
  • 主にイベント リスナーの実行に時間が費やされました(attribution.processingDuration と合計指標 value を比較)。
  • 遅いイベント リスナー コードは、third-party/cmp.jsscripts.sourceURL から)で定義されたクリック リスナーから始まります。

これで、最適化が必要な場所を特定するのに十分なデータが得られました。

6. 複数のイベント リスナー

DevTools コンソールがクリアになり、Cookie 同意の操作が最長の操作ではなくなるように、ページを更新します。

検索ボックスに文字を入力します。アトリビューション データには何が表示されますか?何が起きていると思いますか?

アトリビューション データ

まず、デモのテストの例を大まかに見てみましょう。

{
  name: 'INP',
  value: 1072,
  rating: 'poor',
  attribution: {
    interactionTargetElement: Element,
    interactionTarget: '#search-terms',
    interactionType: 'keyboard',

    inputDelay: 3.3,
    processingDuration: 1060.6,
    presentationDelay: 8.1,

    processedEventEntries: [...],
    longAnimationFrameEntries: [...],
  }
}

これは、input#search-terms 要素とのキーボード操作による INP 値の低さ(CPU スロットリングが有効)を示しています。時間の大部分(合計 INP 1,072 ミリ秒のうち 1,061 ミリ秒)は処理時間に費やされました。

ただし、scripts エントリはより興味深いものです。

レイアウト スラッシング

scripts 配列の最初のエントリから、次のような貴重なコンテキストが得られます。

scripts: [{
  name: 'script',
  invoker: 'BUTTON#confirm.onclick',
  invokerType: 'event-listener',

  startTime: 4875.6,
  executionStart: 4875.6,
  duration: 497,
  forcedStyleAndLayoutDuration: 388,

  sourceURL: 'http://localhost:8080/js/index.js',
  sourceFunctionName: 'handleSearch',
  sourceCharPosition: 940
},
...]

処理時間の大部分は、このスクリプトの実行中(input リスナー、呼び出し元は INPUT#search-terms.oninput)に発生します。関数名(handleSearch)と index.js ソースファイル内の文字位置が指定されています。

ただし、forcedStyleAndLayoutDuration という新しいプロパティがあります。これは、ブラウザがページの再レイアウトを強制された、このスクリプト呼び出し内の時間です。つまり、このイベント リスナーの実行に費やされた時間の 78%(497 ミリ秒のうち 388 ミリ秒)は、レイアウト スラッシングに費やされたことになります。

これは最優先で修正する必要があります。

繰り返しリスナー

次の 2 つのスクリプト エントリは、個別に見て特に注目すべき点はありません。

scripts: [...,
{
  name: 'script',
  invoker: '#document.onkeyup',
  invokerType: 'event-listener',

  startTime: 5375.3,
  executionStart: 5375.3,
  duration: 124,

  sourceURL: 'http://localhost:8080/js/index.js',
  sourceFunctionName: '',
  sourceCharPosition: 1526,
},
{
  name: 'script',
  invoker: '#document.onkeyup',
  invokerType: 'event-listener',

  startTime: 5673.9,
  executionStart: 5673.9,
  duration: 95,

  sourceURL: 'http://localhost:8080/js/index.js',
  sourceFunctionName: '',
  sourceCharPosition: 1526
}]

両方のエントリは keyup リスナーであり、連続して実行されます。リスナーは匿名関数であるため(sourceFunctionName プロパティには何も報告されません)、ソースファイルと文字位置は引き続き存在するため、コードの場所を特定できます。

奇妙なのは、両方とも同じソースファイルと文字位置から来ていることです。

ブラウザは、1 つのアニメーション フレームで複数のキープレスを処理することになり、このイベント リスナーは、描画が行われる前に 2 回実行されることになりました。

この影響は複合的に発生することもあります。イベント リスナーの完了に時間がかかるほど、追加の入力イベントがより多く発生し、遅延したインタラクションがさらに長くなります。

これは検索/オートコンプリートのインタラクションであるため、入力のデバウンスは、フレームごとに最大 1 回のキープレスが処理されるようにするための優れた戦略です。

7. 入力遅延

入力遅延(ユーザーが操作してからイベント リスナーが操作の処理を開始するまでの時間)の一般的な原因は、メインスレッドがビジー状態であることです。これには複数の原因が考えられます。

  • ページが読み込まれ、メインスレッドが DOM の設定、ページのレイアウトとスタイルの設定、スクリプトの評価と実行という初期作業でビジー状態になっている。
  • ページが一般的にビジー状態である(計算、スクリプトベースのアニメーション、広告の実行など)。
  • 前回のインタラクションの処理に時間がかかり、今後のインタラクションが遅延する(最後の例を参照)。

デモページには、ページ上部のカタツムリのロゴをクリックするとアニメーションが始まり、メインスレッドで JavaScript の重い処理を行うという秘密の機能があります。

  • カタツムリのロゴをクリックしてアニメーションを開始します。
  • JavaScript タスクは、カタツムリがバウンドの最下部に達したときにトリガーされます。バウンスの直前でページを操作し、INP がどの程度高くなるかを確認します。

たとえば、カタツムリが跳ね返る直前に検索ボックスをクリックしてフォーカスを当てるなど、他のイベント リスナーをトリガーしなかったとしても、メインスレッドの処理によってページがしばらくの間応答しなくなります。

多くのページでは、メインスレッドの処理がこれほど適切に行われることはありませんが、INP 属性データでどのように識別できるかを確認するうえで、このデモは有効です。

カタツムリのバウンス中に検索ボックスのみにフォーカスした場合の帰属の例を次に示します。

{
  name: 'INP',
  value: 728,
  rating: 'poor',

  attribution: {
    interactionTargetElement: Element,
    interactionTarget: '#search-terms',
    interactionType: 'pointer',

    inputDelay: 702.3,
    processingDuration: 4.9,
    presentationDelay: 20.8,

    longAnimationFrameEntries: [{
      name: 'long-animation-frame',
      startTime: 2064.8,
      duration: 790,

      renderStart: 2065,
      styleAndLayoutStart: 2854.2,
      firstUIEventTimestamp: 0,
      blockingDuration: 740,

      scripts: [{...}]
    }]
  }
}

予測どおり、イベント リスナーはすばやく実行され、処理時間は 4.9 ミリ秒でした。インタラクションの大部分は入力遅延に費やされ、合計 728 ミリ秒のうち 702.3 ミリ秒を占めていました。

この状況ではデバッグが難しくなることがあります。ユーザーが何にどのように操作したかはわかっていますが、その操作はすぐに完了し、問題は発生していません。実際には、ページ上の別の要素が処理の開始を遅らせていたのですが、どこから調べ始めればよいのでしょうか?

LoAF スクリプト エントリは、次のようにして問題を解決します。

scripts: [{
  name: 'script',
  invoker: 'SPAN.onanimationiteration',
  invokerType: 'event-listener',

  startTime: 2065,
  executionStart: 2065,
  duration: 788,

  sourceURL: 'http://localhost:8080/js/index.js',
  sourceFunctionName: 'cryptodaphneCoinHandler',
  sourceCharPosition: 1831
}]

この関数はインタラクションとは関係ありませんが、アニメーション フレームを遅くしたため、インタラクション イベントと結合された LoAF データに含まれています。

ここから、インタラクション処理を遅延させた関数が(animationiteration リスナーによって)どのようにトリガーされたか、どの関数が担当したか、ソースファイル内のどこに配置されたかを確認できます。

8. プレゼンテーションの遅延: 更新が描画されない場合

プレゼンテーション遅延は、イベント リスナーの実行が完了してから、ブラウザが新しいフレームを画面に描画してユーザーにフィードバックを表示できるようになるまでの時間を測定します。

ページを更新して INP 値を再度リセットし、ハンバーガー メニューを開きます。開くときに明確な引っ掛かりがあります。

これはどのような状況でしょうか。

{
  name: 'INP',
  value: 376,
  rating: 'needs-improvement',
  delta: 352,

  attribution: {
    interactionTarget: '#sidenav-button>svg',
    interactionType: 'pointer',

    inputDelay: 12.8,
    processingDuration: 14.7,
    presentationDelay: 348.5,

    longAnimationFrameEntries: [{
      name: 'long-animation-frame',
      startTime: 651,
      duration: 365,

      renderStart: 673.2,
      styleAndLayoutStart: 1004.3,
      firstUIEventTimestamp: 138.6,
      blockingDuration: 315,

      scripts: [{...}]
    }]
  }
}

今回は、遅延の大部分を占めるのはプレゼンテーションの遅延です。つまり、メインスレッドをブロックするものは、イベント リスナーが完了した後に発生します。

scripts: [{
  entryType: 'script',
  invoker: 'FrameRequestCallback',
  invokerType: 'user-callback',

  startTime: 673.8,
  executionStart: 673.8,
  duration: 330,

  sourceURL: 'http://localhost:8080/js/side-nav.js',
  sourceFunctionName: '',
  sourceCharPosition: 1193,
}]

scripts 配列の単一のエントリを見ると、FrameRequestCallbackuser-callback で時間が費やされていることがわかります。今回は、requestAnimationFrame コールバックが原因でプレゼンテーションの遅延が発生しています。

9. まとめ

フィールド データの集計

これは、単一のページ読み込みからの単一の INP 帰属エントリを調べるときに、すべてが簡単になることを認識しておく必要があります。このデータを集計して、フィールド データに基づいて INP をデバッグするにはどうすればよいですか?詳細な情報が多すぎて、かえって難しくなっています。

たとえば、どのページ要素がインタラクションの遅延の一般的な原因となっているかを知ることは非常に有用です。ただし、ページにビルドごとに変更されるコンパイル済みの CSS クラス名がある場合、同じ要素の web-vitals セレクタがビルド間で異なることがあります。

代わりに、特定のアプリケーションを検討して、最も有用なものとデータの集計方法を判断する必要があります。たとえば、アトリビューション データをビーコンで送信する前に、ターゲットが属するコンポーネントやターゲットが満たす ARIA ロールに基づいて、web-vitals セレクタを独自の識別子に置き換えることができます。

同様に、scripts エントリの sourceURL パスにファイルベースのハッシュが含まれていると、結合が難しくなる可能性がありますが、既知のビルドプロセスに基づいてハッシュを削除してから、データをビーコンで送信し直すことができます。

残念ながら、これほど複雑なデータで簡単な方法はありませんが、デバッグ プロセスでは、データの一部を使用するだけでも、アトリビューション データがまったくない場合よりも価値があります。

アトリビューションはあらゆる場所で利用できます。

LoAF ベースの INP アトリビューションは、強力なデバッグ支援機能です。INP の間に具体的に何が起こったかについての詳細なデータを提供します。多くの場合、最適化の取り組みを開始するスクリプト内の正確な場所を特定できます。

これで、どのサイトでも INP アトリビューション データを使用できるようになりました。

ページを編集する権限がない場合でも、DevTools コンソールで次のスニペットを実行して、この Codelab のプロセスを再現し、検出結果を確認できます。

const script = document.createElement('script');
script.src = 'https://unpkg.com/web-vitals@4/dist/web-vitals.attribution.iife.js';
script.onload = function () {
  webVitals.onINP(console.log, {reportAllChanges: true});
};
document.head.appendChild(script);

その他の情報