ユーザー中心の指標を測定できることは、あらゆるウェブサイトで普遍的に大きな価値があります。これらの指標を使用すると、次のことが可能になります。
- 実際のユーザーがウェブ全体をどのように利用しているかを把握できます。
- 競合他社のサイトと比較します。
- カスタムコードを記述しなくても、分析ツールで有用で実用的なデータをトラッキングできます。
ユニバーサル指標は適切なベースラインを提供しますが、特定のサイトの全体的なユーザー エクスペリエンスを把握するには、多くの場合、これらの指標だけでなく他の指標も測定する必要があります。
カスタム指標を使用すると、サイトにのみ適用される可能性がある次のようなサイトのエクスペリエンスを測定できます。
- シングルページ アプリ(SPA)がある「ページ」から別の「ページ」に移行するまでにかかる時間。
- ログイン ユーザー向けにデータベースから取得したデータをページに表示するのにかかる時間。
- サーバーサイド レンダリング(SSR)アプリがハイドレートするのにかかる時間。
- リピーターが読み込んだリソースのキャッシュ ヒット率。
- ゲーム内のクリック イベントまたはキーボード イベントのイベント レイテンシ。
カスタム指標を測定する API
これまでウェブ デベロッパーは、パフォーマンスを測定するための低レベルの API をあまり利用できませんでした。そのため、サイトのパフォーマンスを測定するためにハックに頼らざるを得ませんでした。
たとえば、requestAnimationFrame ループを実行して各フレーム間のデルタを計算することで、長時間実行されている JavaScript タスクが原因でメインスレッドがブロックされているかどうかを判断できます。デルタがディスプレイのフレームレートよりも大幅に長い場合は、長いタスクとして報告できます。ただし、このようなハックは、バッテリーの消耗など、パフォーマンス自体に影響するため、おすすめしません。
効果的なパフォーマンス測定の最初のルールは、パフォーマンス測定手法自体がパフォーマンスの問題を引き起こしていないことを確認することです。そのため、サイトで測定するカスタム指標については、可能であれば次のいずれかの API を使用することをおすすめします。
Performance Observer API
Performance Observer API は、このページで説明する他のすべてのパフォーマンス API からデータを収集して表示するメカニズムです。優れたデータを取得するには、この点を理解することが重要です。
PerformanceObserver を使用して、パフォーマンス関連のイベントをパッシブにサブスクライブできます。これにより、API コールバックはアイドル期間中に実行されるため、通常はページ パフォーマンスに影響しません。
PerformanceObserver を作成するには、新しいパフォーマンス エントリがディスパッチされるたびに実行されるコールバックを渡します。次に、observe() メソッドを使用して、オブザーバーにリッスンするエントリのタイプを伝えます。
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Log the entry and all associated details.
console.log(entry.toJSON());
}
});
po.observe({type: 'some-entry-type'});
以降のセクションでは、オブザーバーで使用できるさまざまなエントリ タイプをすべて示しますが、新しいブラウザでは、静的 PerformanceObserver.supportedEntryTypes プロパティを使用して、使用可能なエントリ タイプを調べることができます。
すでに発生したエントリを観察する
デフォルトでは、PerformanceObserver オブジェクトはエントリが発生したときにのみエントリを監視できます。パフォーマンス分析コードを遅延読み込みして、優先度の高いリソースをブロックしないようにする場合、このことが問題になることがあります。
過去のエントリ(発生後)を取得するには、observe() を呼び出すときに buffered フラグを true に設定します。ブラウザは、PerformanceObserver コールバックが最初に呼び出されたときに、パフォーマンス エントリ バッファから、そのタイプの最大バッファサイズまでの履歴エントリを含めます。
po.observe({
type: 'some-entry-type',
buffered: true,
});
避けるべき以前のパフォーマンス API
Performance Observer API の前は、デベロッパーは performance オブジェクトで定義された次の 3 つの方法を使用してパフォーマンス エントリにアクセスできました。
これらの API は引き続きサポートされますが、新しいエントリが発行されたときにリッスンできないため、使用はおすすめしません。また、largest-contentful-paint などの多くの新しい API は performance オブジェクトを介して公開されず、PerformanceObserver を介してのみ公開されます。
Internet Explorer との互換性が特に必要な場合を除き、コードでこれらのメソッドを使用することは避け、今後は PerformanceObserver を使用することをおすすめします。
User Timing API
User Timing API は、時間ベースの指標を測定するための汎用 API です。任意の時点でポイントをマークし、後でそれらのマーク間の期間を測定できます。
// Record the time immediately before running a task.
performance.mark('myTask:start');
await doMyTask();
// Record the time immediately after running a task.
performance.mark('myTask:end');
// Measure the delta between the start and end of the task
performance.measure('myTask', 'myTask:start', 'myTask:end');
Date.now() や performance.now() などの API でも同様の機能を利用できますが、User Timing API を使用するメリットは、パフォーマンス ツールとの統合が容易なことです。たとえば、Chrome DevTools ではパフォーマンス パネルでユーザー タイミングの測定値を可視化します。また、多くの分析プロバイダは、ユーザーが行った測定値を自動的に追跡し、期間データを分析バックエンドに送信します。
ユーザー タイミングの測定値をレポートするには、PerformanceObserver を使用して、measure タイプのエントリを監視するように登録します。
// Create the performance observer.
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Log the entry and all associated details.
console.log(entry.toJSON());
}
});
// Start listening for `measure` entries to be dispatched.
po.observe({type: 'measure', buffered: true});
Long Tasks API
Long Tasks API は、ブラウザのメインスレッドがフレームレートや入力遅延に影響するほど長くブロックされたタイミングを把握するのに役立ちます。API は、50 ミリ秒を超えて実行されるタスクを報告します。
コストの高いコードを実行したり、大きなスクリプトを読み込んで実行したりする必要がある場合は、そのコードがメインスレッドをブロックするかどうかを追跡すると便利です。実際、多くの高レベルの指標は、Long Tasks API 自体(Time to Interactive(TTI)や Total Blocking Time(TBT)など)に基づいて構築されています。
長いタスクが発生するタイミングを特定するには、PerformanceObserver を使用して、longtask タイプのエントリを監視するように登録します。
// Create the performance observer.
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Log the entry and all associated details.
console.log(entry.toJSON());
}
});
// Start listening for `longtask` entries to be dispatched.
po.observe({type: 'longtask', buffered: true});
Long Animation Frames API
Long Animation Frames API は、Long Tasks API の新しいイテレーションです。50 ミリ秒を超える長いタスクではなく、長いフレームを調べます。これにより、より優れたアトリビューションや、問題が発生する可能性のある遅延の範囲の拡大など、Long Tasks API のいくつかの欠点が解消されます。
長いフレームが発生するタイミングを特定するには、PerformanceObserver を使用して、long-animation-frame タイプのエントリを監視するように登録します。
// Create the performance observer.
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Log the entry and all associated details.
console.log(entry.toJSON());
}
});
// Start listening for `long-animation-frame` entries to be dispatched.
po.observe({type: 'long-animation-frame', buffered: true});
Element Timing API
Largest Contentful Paint(LCP)指標は、最も大きな画像またはテキスト ブロックが画面に描画されたタイミングを知るのに役立ちますが、場合によっては別の要素のレンダリング時間を測定したいことがあります。
このような場合は、Element Timing API を使用します。LCP API は実際には Element Timing API の上に構築されており、最大コンテンツ要素の自動レポート機能が追加されています。ただし、他の要素に elementtiming 属性を明示的に追加し、element エントリ タイプを監視する PerformanceObserver を登録することで、他の要素をレポートすることもできます。
<img elementtiming="hero-image" />
<p elementtiming="important-paragraph">This is text I care about.</p>
<!-- ... -->
<script>
const po = new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
// Log the entry and all associated details.
console.log(entry.toJSON());
}
});
// Start listening for `element` entries to be dispatched.
po.observe({type: 'element', buffered: true});
</script>
Event Timing API
Interaction to Next Paint(INP)指標は、ページのライフサイクル全体にわたるすべてのクリック、タップ、キーボード操作をモニタリングすることで、ページの全体的な応答性を評価します。ページの INP は、ユーザーが操作を開始してから、ブラウザがユーザーの入力の視覚的な結果を示す次のフレームをペイントするまでの時間が最も長かった操作であることがほとんどです。
INP 指標は Event Timing API によって実現されています。この API は、イベントのライフサイクル中に発生するさまざまなタイムスタンプを公開します。たとえば、次のようなタイムスタンプがあります。
startTime: ブラウザがイベントを受信した時刻。processingStart: ブラウザがイベントのイベント ハンドラの処理を開始できる時刻。processingEnd: ブラウザがこのイベントのイベント ハンドラから開始されたすべての同期コードの実行を終了した時間。duration: ブラウザがイベントを受信してから、イベント ハンドラから開始されたすべての同期コードの実行が完了し、次のフレームをペイントできるようになるまでの時間(セキュリティ上の理由から 8 ミリ秒単位で丸められます)。
次の例は、これらの値を使用してカスタム測定を作成する方法を示しています。
const po = new PerformanceObserver((entryList) => {
// Get the last interaction observed:
const entries = Array.from(entryList.getEntries()).forEach((entry) => {
// Get various bits of interaction data:
const inputDelay = entry.processingStart - entry.startTime;
const processingTime = entry.processingEnd - entry.processingStart;
const presentationDelay = entry.startTime + entry.duration - entry.processingEnd;
const duration = entry.duration;
const eventType = entry.name;
const target = entry.target || "(not set)"
console.log("----- INTERACTION -----");
console.log(`Input delay (ms): ${inputDelay}`);
console.log(`Event handler processing time (ms): ${processingTime}`);
console.log(`Presentation delay (ms): ${presentationDelay}`);
console.log(`Total event duration (ms): ${duration}`);
console.log(`Event type: ${eventType}`);
console.log(target);
});
});
// A durationThreshold of 16ms is necessary to include more
// interactions, since the default is 104ms. The minimum
// durationThreshold is 16ms.
po.observe({type: 'event', buffered: true, durationThreshold: 16});
Resource Timing API
Resource Timing API を使用すると、特定のページの各リソースがどのように読み込まれたかについて、デベロッパーが詳細な分析情報を得ることができます。API の名前とは異なり、提供される情報はタイミング データだけではありません(タイミング データもたくさんあります)。アクセスできるその他のデータには、次のものがあります。
initiatorType: リソースの取得方法(<script>タグ、<link>タグ、fetch()呼び出しなど)。nextHopProtocol: リソースの取得に使用されるプロトコル(h2やquicなど)。encodedBodySize/decodedBodySize]: リソースのサイズ(それぞれエンコード形式またはデコード形式)transferSize: ネットワーク経由で実際に転送されたリソースのサイズ。リソースがキャッシュによって満たされる場合、この値はencodedBodySizeよりもはるかに小さくなることがあります。また、キャッシュの再検証が必要ない場合は、ゼロになることもあります。
リソース タイミング エントリの transferSize プロパティを使用すると、キャッシュ ヒット率指標やキャッシュに保存されたリソースの合計サイズ指標を測定できます。これは、リソース キャッシュ保存戦略がリピーターのパフォーマンスにどのように影響するかを把握するうえで役立ちます。
次の例では、ページでリクエストされたすべてのリソースをログに記録し、各リソースがキャッシュによって満たされたかどうかを示します。
// Create the performance observer.
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// If transferSize is 0, the resource was fulfilled using the cache.
console.log(entry.name, entry.transferSize === 0);
}
});
// Start listening for `resource` entries to be dispatched.
po.observe({type: 'resource', buffered: true});
Navigation Timing API
Navigation Timing API は Resource Timing API と似ていますが、ナビゲーション リクエストのみをレポートします。navigation エントリ タイプも resource エントリ タイプと似ていますが、ナビゲーション リクエストにのみ固有の追加情報(DOMContentLoaded イベントと load イベントが発火したタイミングなど)が含まれています。
多くのデベロッパーがサーバーの応答時間(最初のバイトまでの時間(TTFB))を把握するために追跡している指標の 1 つは、Navigation Timing API を使用して取得できます。具体的には、エントリの responseStart タイムスタンプです。
// Create the performance observer.
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// If transferSize is 0, the resource was fulfilled using the cache.
console.log('Time to first byte', entry.responseStart);
}
});
// Start listening for `navigation` entries to be dispatched.
po.observe({type: 'navigation', buffered: true});
Service Worker を使用するデベロッパーが気にする可能性のあるもう 1 つの指標は、ナビゲーション リクエストの Service Worker の起動時間です。ブラウザがサービス ワーカー スレッドを開始し、フェッチ イベントのインターセプトを開始するまでの時間です。
特定のナビゲーション リクエストの Service Worker の起動時間は、entry.responseStart と entry.workerStart の差分から判断できます。
// Create the performance observer.
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('Service Worker startup time:',
entry.responseStart - entry.workerStart);
}
});
// Start listening for `navigation` entries to be dispatched.
po.observe({type: 'navigation', buffered: true});
Server Timing API
Server Timing API を使用すると、リクエスト固有のタイミング データをレスポンス ヘッダーを通じてサーバーからブラウザに渡すことができます。たとえば、特定のリクエストでデータベース内のデータを検索するのに要した時間を示すことができます。これは、サーバーの遅延によって発生したパフォーマンスの問題をデバッグする際に役立ちます。
サードパーティの分析プロバイダを使用しているデベロッパーにとって、Server Timing API は、これらの分析ツールが測定している他のビジネス指標とサーバー パフォーマンス データを関連付ける唯一の方法です。
レスポンスでサーバー タイミング データを指定するには、Server-Timing レスポンス ヘッダーを使用します。次に例を示します。
HTTP/1.1 200 OK
Server-Timing: miss, db;dur=53, app;dur=47.2
その後、ページから Resource Timing API と Navigation Timing API の resource エントリと navigation エントリの両方でこのデータを読み取ることができます。
// Create the performance observer.
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Logs all server timing data for this response
console.log('Server Timing', entry.serverTiming);
}
});
// Start listening for `navigation` entries to be dispatched.
po.observe({type: 'navigation', buffered: true});