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

1. はじめに

これは、web-vitals ライブラリを使用して Interaction to Next Paint(INP) を測定する方法を学習するためのインタラクティブな Codelab です。

前提条件

学習内容

  • 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 4x スローダウン] を選択します。

4x CPU 遅延が選択された状態の DevTools の [Performance] パネルのスクリーンショット

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

web-vitals は、ユーザーが利用するウェブに関する指標を測定するための JavaScript ライブラリです。ライブラリを使用してそれらの値をキャプチャし、後で分析するために分析エンドポイントにビーコンできます。これにより、遅いインタラクションがいつどこでどこで発生しているかを特定できます。

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

この Codelab は npm からインストールし、スクリプトを直接読み込みます。これにより、特定のビルドプロセスに入る必要がなくなります。

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

  • 「標準」build は、ページの読み込み時に Core Web Vitals の指標の値をトラッキングする場合に使用します。
  • 「アトリビューション」build は、各指標にデバッグ情報を追加して、指標がその値になった理由を診断します。

この Codelab で INP を測定するには、アトリビューションを構築する必要があります。

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

ページに 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 がトリガーされているため、クリックのやり取りが遅くなります。そういうことだよ。

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

DevTools コンソールにロギングされる INP データ オブジェクト

このトップレベルの情報は、標準版とアトリビューション版の両方の Web- Vitals ビルドで使用できます。

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

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

この時間帯に何が起こっているのかを調べるには、attribution プロパティを使用します。アトリビューション データを作成するために、web-vitals はクリック イベントと重複する長いアニメーション フレーム(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: イベント リスナーが完了するまでにかかった時間。多くの場合、ページでは 1 つのイベントに対して複数のリスナーを使用できます(例: pointerdownpointerupclick)。これらがすべて同じアニメーション フレームで実行されると、今回は 1 つにまとめられます。この場合、処理時間は INP 時間の大部分である 295.6 ミリ秒に及びます。
  • presentationDelay: イベント リスナーが完了してから、ブラウザが次のフレームの描画を完了するまでの時間。この例では 21.4 ミリ秒です。

これらの INP フェーズは、最適化の必要があるものを診断するための重要なシグナルとなります。このテーマについて詳しくは、オプティマイズの INP ガイドをご覧ください。

もう少し詳しく見ると、processedEventEntries には 5 つのイベントが含まれています。これに対して、トップレベルの INP entries 配列には 1 つのイベントがあります。の機能上の違い

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 配列です。これは単一のエントリの場合もありますが、インタラクションが複数のフレームに広がる場合もあります。ここでは、長いアニメーション フレームが 1 つある最も単純なケースを考えてみましょう。

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
}]

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

重要なポイント

このケースについて、このアトリビューション データから何を判断できますか?

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

これで、最適化が必要な箇所を把握できる十分なデータが得られました。

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 スロットリングが有効な場合)です。時間の大半(1,072 ミリ秒の合計 INP のうち 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 リスナーで、1 つずつ順番に実行されます。リスナーは匿名関数です(そのため、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 配列の 1 つのエントリを見ると、FrameRequestCallbackuser-callback で費やされている時間がわかります。今回は、プレゼンテーションの遅延が requestAnimationFrame コールバックによって生じています。

9. まとめ

フィールド データの集計

1 回のページ読み込みで 1 つの 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);

その他の情報