あらゆるウェブサイトで測定できる、普遍的でユーザー中心の指標があると、ユーザーのウェブ体験を把握したり、競合他社のサイトと比較したりするうえで非常に役立ちます。しかし多くの場合、特定のサイトの全体的なエクスペリエンスを把握するためには、一般的な指標以外の指標も測定する必要があります。
カスタム指標を使用すると、サイトだけに当てはまる次のようなエクスペリエンスの要素を測定できます。
- シングルページ アプリ(SPA)がある「ページ」から別の「ページ」に移行するのにかかる時間。
- ログインしているユーザーのデータベースから取得したデータがページに表示されるまでの時間。
- サーバーサイド レンダリング(SSR)アプリのハイドレートに要する時間。
- リピーターによって読み込まれたリソースのキャッシュ ヒット率。
- ゲーム内のクリック イベントまたはキーボード イベントのイベント レイテンシ。
カスタム指標を測定するための API
これまでウェブ デベロッパーはパフォーマンスを測定するための低レベル API をあまり使用していませんでした。そのため、ウェブ デベロッパーはサイトのパフォーマンスを測定するためにハッキングに頼る必要がありました。たとえば、requestAnimationFrame
ループを実行して各フレーム間の差分を計算することで、長時間実行される JavaScript タスクによってメインスレッドがブロックされているかどうかを判断できます。差分がディスプレイのフレームレートより大幅に長い場合は、長いタスクとして報告できます。
ただし、このようなハッキングは、デバイスのバッテリーが消耗するなど、サイトのパフォーマンスに影響する可能性があります。パフォーマンス測定手法自体がパフォーマンスの問題を引き起こす場合、その手法から得られるデータは正確ではありません。したがって、カスタム指標を作成するには、次のいずれかの API を使用することをおすすめします。
Performance Observer API
Performance Observer API は、このページで説明する他のすべてのパフォーマンス API からデータを収集して表示するメカニズムです。優れたデータを得るには、この理解が不可欠です。
PerformanceObserver
を使用すると、パフォーマンス関連のイベントを受動的にサブスクライブできます。これにより、アイドル期間に API コールバックを呼び出せるようになり、通常はページのパフォーマンスが妨げられることはありません。
PerformanceObserver
を作成するときに、新しいパフォーマンス エントリがディスパッチされるたびに実行されるコールバックを渡します。次に、次のように observe()
メソッドを使用して、リッスンするエントリのタイプをオブザーバーに指示します。
// Catch errors that some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
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'});
} catch (e) {
// Do nothing if the browser doesn't support this API.
}
以降のセクションでは、確認できるすべてのエントリタイプを示します。新しいブラウザでは、静的 PerformanceObserver.supportedEntryTypes
プロパティを使用して、使用可能なエントリタイプを調べることもできます。
すでに発生しているエントリを確認する
デフォルトでは、PerformanceObserver
オブジェクトは発生したエントリのみを監視できます。これにより、優先度の高いリソースをブロックしないようにパフォーマンス分析コードを遅延読み込みすると、問題が発生する可能性があります。
履歴エントリを取得するには、buffered
フラグを true
に設定して observe
を呼び出します。これにより、PerformanceObserver
コールバックが初めて呼び出されたときに、ブラウザはパフォーマンス エントリ バッファの履歴エントリを含めます。
po.observe({
type: 'some-entry-type',
buffered: true,
});
避けるべき従来のパフォーマンス API
Performance Observer API を使用する前は、デベロッパーは performance
オブジェクトで定義された次のメソッドを使用して、パフォーマンス エントリにアクセスできました。新しいエントリをリッスンできないため、使用はおすすめしません。
また、多くの新しい API(長いタスクなど)は、performance
オブジェクトでは公開されず、PerformanceObserver
でのみ公開されています。そのため、特に Internet Explorer との互換性が必要な場合を除き、コード内でこれらのメソッドを使用せず、今後は PerformanceObserver
を使用することをおすすめします。
カスタム速度 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
タイプのエントリを監視します。
// Catch errors some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
// 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.
po.observe({type: 'measure', buffered: true});
} catch (e) {
// Do nothing if the browser doesn't support this API.
}
Long Tasks API
Long Tasks API は、フレームレートや入力レイテンシに影響を与えるほどブラウザのメインスレッドが長時間ブロックされたかどうかを判断するのに役立ちます。この API は、実行時間が 50 ミリ秒(ms)を超えるタスクを報告します。
コストの高いコードを実行したり、大規模なスクリプトを読み込んで実行したりする必要がある場合は、そのコードがメインスレッドをブロックしているかどうかを追跡すると便利です。実際、多くの上位レベルの指標(Time to Interactive(TTI)やTotal Blocking Time(TBT)など)は Long Tasks API の上に構築されます。
長時間のタスクが発生するタイミングを判断するには、PerformanceObserver を登録して longtask
タイプのエントリを監視します。
// Catch errors since some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
// 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});
} catch (e) {
// Do nothing if the browser doesn't support this API.
}
Element Timing API
Largest Contentful Paint(LCP)指標は、ページ上の最も大きな画像またはテキスト ブロックがいつ画面に描画されるかを知るのに便利ですが、場合によっては別の要素のレンダリング時間を測定したいこともあります。
このような場合は、Element Timing API を使用してください。
LCP API は実際には Element Timing API の上に構築されており、最も大きなコンテンツがある要素の自動レポートを追加しますが、他の要素に elementtiming
属性を明示的に追加し、PerformanceObserver を登録して element
エントリタイプを監視することもできます。
<img elementtiming="hero-image" />
<p elementtiming="important-paragraph">This is text I care about.</p>
...
<script>
// Catch errors since some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
// Create the performance observer.
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});
} catch (e) {
// Do nothing if the browser doesn't support this API.
}
</script>
Event Timing API
First Input Delay(FID)の指標は、ユーザーが最初にページを操作してから、その操作に対するレスポンスとしてブラウザがイベント ハンドラの処理を開始するまでの時間を測定します。ただし、イベント処理時間自体を測定すると役立つ場合もあります。
これは、Event Timing API を使用して可能です。この API は、FID の測定に加えて、イベントのライフサイクルで次のような多くのタイムスタンプを公開します。
startTime
: ブラウザがイベントを受信した時刻。processingStart
: ブラウザがイベントのイベント ハンドラの処理を開始できる時刻。processingEnd
: ブラウザがこのイベントのイベント ハンドラから開始された同期コードをすべて完了した時刻。duration
: ブラウザがイベントを受け取ってから、イベント ハンドラから開始されたすべての同期コードの実行を終了してから次のフレームを描画できるようになるまでの時間(セキュリティ上の理由から 8 ミリ秒に丸められます)。
次の例は、これらの値を使用してカスタム測定を作成する方法を示しています。
// Catch errors some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
const po = new PerformanceObserver((entryList) => {
const firstInput = entryList.getEntries()[0];
// Measure First Input Delay (FID).
const firstInputDelay = firstInput.processingStart - firstInput.startTime;
// Measure the time it takes to run all event handlers
// Doesn't include work scheduled asynchronously using methods like
// `requestAnimationFrame()` or `setTimeout()`.
const firstInputProcessingTime = firstInput.processingEnd - firstInput.processingStart;
// Measure the entire duration of the event, from when input is received by
// the browser until the next frame can be painted after processing all
// event handlers.
// Doesn't include work scheduled asynchronously using
// `requestAnimationFrame()` or `setTimeout()`.
// For security reasons, this value is rounded to the nearest 8 ms.
const firstInputDuration = firstInput.duration;
// Log these values to the console.
console.log({
firstInputDelay,
firstInputProcessingTime,
firstInputDuration,
});
});
po.observe({type: 'first-input', buffered: true});
} catch (error) {
// Do nothing if the browser doesn't support this API.
}
Resource Timing API
Resource Timing API は、特定のページのリソースがどのように読み込まれたかをデベロッパーに提供します。API の名前に反して、提供される情報はタイミング データだけに限りません(ただし、情報が多数あります)。アクセスできるその他のデータには、次のようなものがあります。
- initiatorType: リソースが取得された方法(
<script>
タグ、<link>
タグ、fetch()
など)。 - nextHopProtocol: リソースの取得に使用されるプロトコル(
h2
やquic
など)。 - encodedBodySize と decodedBodySize]: それぞれエンコードまたはデコードされた形式でのリソースのサイズ。
- transferSize: ネットワーク経由で実際に転送されたリソースのサイズ。キャッシュを使用してリソースが処理される場合、この値は
encodedBodySize
よりはるかに小さくなる可能性があり、キャッシュの再検証が不要な場合は、ゼロになることもあります。
リソースのタイミング エントリの transferSize
プロパティを使用して、キャッシュ ヒット率の指標またはキャッシュされたリソースの合計サイズの指標を測定できます。これらの指標は、リソース キャッシュ戦略がリピーターのパフォーマンスに及ぼす影響を理解するのに役立ちます。
次の例では、ページによってリクエストされたすべてのリソースがログに記録され、各リソースがキャッシュを使用して処理されたかどうかを示します。
// Catch errors some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
// Create the performance observer.
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// If transferSize is 0, the resource was fulfilled via the cache.
console.log(entry.name, entry.transferSize === 0);
}
});
// Start listening for `resource` entries to be dispatched.
po.observe({type: 'resource', buffered: true});
} catch (e) {
// Do nothing if the browser doesn't support this API.
}
Navigation Timing API
Navigation Timing API は Resource Timing API と似ていますが、ナビゲーション リクエストのみをレポートします。
navigation
エントリタイプも resource
エントリタイプに似ていますが、ナビゲーション リクエスト(DOMContentLoaded
イベントや load
イベントが発生したときなど)のみに固有の追加情報が含まれています。
サーバーの応答時間を把握するために多くのデベロッパーが追跡している指標の 1 つである Time to First Byte(TTFB)は、Navigation Timing API の responseStart
タイムスタンプで確認できます。
// Catch errors since browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
// 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});
} catch (e) {
// Do nothing if the browser doesn't support this API.
}
Service Worker を使用するデベロッパーにとって重要になるもう一つの指標は、ナビゲーション リクエストの Service Worker の起動時間です。これは、ブラウザが取得イベントのインターセプトを開始する前に、Service Worker スレッドを開始するのにかかる時間です。
指定されたナビゲーション リクエストの Service Worker の起動時間は、次のように entry.responseStart
と entry.workerStart
の差分から求めることができます。
// Catch errors some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
// 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});
} catch (e) {
// Do nothing if the browser doesn't support this API.
}
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
エントリの両方で、このデータを読み取ることができます。
// Catch errors some browsers throw when using the new `type` option.
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
// 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});
} catch (e) {
// Do nothing if the browser doesn't support this API.
}