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

1. はじめに

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

前提条件

学習内容

  • web-vitals ライブラリをページに追加して、そのアトリビューション データを使用するための方法。
  • アトリビューション データを使用して、インプレッション数を改善する場所と方法を診断します。

必要なもの

  • 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 倍の遅延] を選択します。

CPU の遅延が 4 倍に設定されたアプリと、デベロッパー ツールの [パフォーマンス] パネルのスクリーンショット

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

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

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

この Codelab では、特定のビルドプロセスに深入りせずに、npm からインストールしてスクリプトを直接読み込みます。

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

  • ページ読み込み時の Core Web Vitals の指標値を追跡する場合は、「標準」ビルドを使用する必要があります。
  • 「アトリビューション」ビルドでは、各指標に追加のデバッグ情報が追加され、指標の値が最終的にどうなったかを診断できます。

この 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 はクリック イベントと重複する長いアニメーション フレーム(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 など)があります。これらがすべて同じアニメーション フレームで実行されると、この時間に統合されます。この場合、処理時間は 295.6 ミリ秒で、INP 時間の大部分を占めています。
  • 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.interactionTarget とスクリプト アトリビューション エントリの invoker プロパティ)のクリックによってトリガーされた。
  • 時間は主にイベント リスナーの実行に費やされています(合計指標 value と比較した attribution.processingDuration から)。
  • 遅いイベント リスナー コードは、third-party/cmp.js で定義されたクリック リスナー(scripts.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. まとめ

フィールドデータの集計

1 回のページ読み込みで 1 つの INP アトリビューション エントリを確認すると、すべてが簡単に把握できます。このデータを集計して、フィールドデータに基づいて INP をデバッグするにはどうすればよいですか?実際には、有用な詳細情報が多いほど、この作業は難しくなります。

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

代わりに、特定のアプリケーションについて考え、最も有用なものとデータを集計する方法を決定する必要があります。たとえば、アトリビューション データをビーコンで返す前に、ターゲットが存在するコンポーネントや、ターゲットが果たす ARIA ロールに基づいて、web-vitals セレクタを独自の ID に置き換えることができます。

同様に、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);

その他の情報