Navigation Timing と Resource Timing で現場での読み込みパフォーマンスを評価する

Navigation API と Resource Timing API を使用して現場の読み込みパフォーマンスを評価するための基礎を学びます。

ブラウザのデベロッパー ツール(または Chrome の Lighthouse)で接続スロットリングを使用して読み込みパフォーマンスを評価したことがある方は、パフォーマンスの調整にこれらのツールがいかに便利であるかをご存知でしょう。安定した安定したベースライン接続速度で、パフォーマンスの最適化による影響をすばやく測定できます。唯一の問題は、これが合成テストであり、フィールド データではなくラボデータが生成されることです。

合成テストは本質的に悪いことではありませんが、実際のユーザーによるウェブサイトの読み込み速度を表すものではありません。そのためにはフィールド データが必要です。これは Navigation Timing API と Resource Timing API から収集できます。

現場の読み込みパフォーマンスの評価に役立つ API

Navigation Timing と Resource Timing は 2 つの異なるものを測定する同様の API で、かなりの部分が重複しています。

  • ナビゲーション タイミングは、HTML ドキュメントに対するリクエスト(ナビゲーション リクエスト)の速度を測定します。
  • リソース タイミングは、CSS、JavaScript、画像など、ドキュメントに依存するリソースのリクエストの速度を測定します。

これらの API のデータは、パフォーマンス エントリ バッファで公開され、ブラウザで JavaScript を使用してアクセスできます。パフォーマンス バッファをクエリする方法は複数ありますが、一般的な方法は performance.getEntriesByType を使用する方法です。

// Get Navigation Timing entries:
performance.getEntriesByType('navigation');

// Get Resource Timing entries:
performance.getEntriesByType('resource');

performance.getEntriesByType は、パフォーマンス エントリ バッファから取得するエントリのタイプを記述する文字列を受け入れます。'navigation''resource' は、それぞれ Navigation Timing API と Resource Timing API のタイミングを取得します。

これらの API で提供される情報の量は膨大になる可能性がありますが、ウェブサイトにアクセスしたユーザーからこれらのタイミングを収集できるため、フィールドでの読み込みパフォーマンスを測定するうえで重要な役割を果たします。

ネットワーク リクエストのライフサイクルとタイミング

ナビゲーションとリソースのタイミングを収集して分析することは、ネットワーク リクエストの一時的な寿命を後から再構築するという、考古学に似ています。概念を視覚化すると役に立つことがあります。ネットワーク リクエストに関して問題がある場合は、ブラウザのデベロッパー ツールが役立ちます。

Chrome の DevTools に表示されるネットワーク タイミングの図。ここに示したタイミングは、リクエストのキューイング、接続のネゴシエーション、リクエスト自体、レスポンス(色分けされたバー)のタイミングです。
Chrome の DevTools の [Network] パネルに表示されたネットワーク リクエストの可視化

ネットワーク リクエストのライフサイクルには、DNS ルックアップ、接続の確立、TLS ネゴシエーションなど、異なるフェーズがあります。これらのタイミングは DOMHighResTimestamp として表されます。ブラウザに応じて、タイミングの粒度はマイクロ秒まで下がったり、ミリ秒に切り上げられたりする場合があります。各フェーズの詳細と、Navigation Timing と Resource Timing との関係を見てみましょう。

DNS ルックアップ

ユーザーが URL にアクセスすると、ドメインを IP アドレスに変換するためにドメイン ネーム システム(DNS)のクエリが行われます。このプロセスはかなりの時間がかかるかもしれません。現場で測定したくなるような時間ですから。Navigation Timing と Resource Timing は、DNS 関連の 2 つのタイミングを公開します。

  • domainLookupStart は DNS ルックアップの開始時です。
  • domainLookupEnd は DNS ルックアップが終了した時点です。

合計 DNS ルックアップ時間を計算するには、終了指標から開始指標を差し引きます。

// Measuring DNS lookup time
const [pageNav] = performance.getEntriesByType('navigation');
const totalLookupTime = pageNav.domainLookupEnd - pageNav.domainLookupStart;

接続ネゴシエーション

読み込みパフォーマンスに影響するもう 1 つの要因は接続ネゴシエーションです。これは、ウェブサーバーに接続するときに発生するレイテンシです。HTTPS が使用されている場合、このプロセスには TLS ネゴシエーション時間も含まれます。接続フェーズは、次の 3 つのタイミングで構成されます。

  • connectStart は、ブラウザがウェブサーバーへの接続を開始するタイミングです。
  • secureConnectionStart は、クライアントが TLS ネゴシエーションを開始するタイミングを示します。
  • connectEnd は、ウェブサーバーへの接続が確立されたときです。

合計接続時間の測定は、DNS ルックアップ時間の合計の測定に似ています。つまり、終了タイミングから開始タイミングを差し引きます。ただし、HTTPS を使用していない場合、または接続が永続的な場合は、追加の secureConnectionStart プロパティが 0 になることがあります。TLS ネゴシエーション時間を測定する場合は、次の点に注意してください。

// Quantifying total connection time
const [pageNav] = performance.getEntriesByType('navigation');
const connectionTime = pageNav.connectEnd - pageNav.connectStart;
let tlsTime = 0; // <-- Assume 0 to start with

// Was there TLS negotiation?
if (pageNav.secureConnectionStart > 0) {
  // Awesome! Calculate it!
  tlsTime = pageNav.connectEnd - pageNav.secureConnectionStart;
}

DNS ルックアップと接続ネゴシエーションが終了すると、ドキュメントの取得とその依存リソースに関連するタイミングが影響を受けます。

リクエストとレスポンス

読み込みのパフォーマンスは、次の 2 種類の要因の影響を受けます。

  • 外的要因: レイテンシや帯域幅などが該当します。ホスティング会社や CDN を選ぶだけでなく、ユーザーはどこからでもウェブにアクセスできるため、(ほとんどの場合)Google の管理外となります。
  • 本質的要素: サーバーサイド アーキテクチャやクライアントサイド アーキテクチャ、リソースサイズ、それらに対して Google が制御できる最適化能力などが該当します。

どちらの種類の要因も読み込みパフォーマンスに影響します。これらの要素に関連するタイミングは、リソースのダウンロードにかかる時間を示すものであり、非常に重要です。Navigation Timing と Resource Timing はどちらも、次の指標を使用して読み込みパフォーマンスを記述します。

  • fetchStart は、ブラウザがリソース(Resource Timing)またはナビゲーション リクエスト用のドキュメント(Navigation Timing)の取得を開始したタイミングをマークします。これは実際のリクエストに先立ち、ブラウザがキャッシュ(HTTP や Cache インスタンスなど)を確認するポイントです。
  • workerStart は、リクエストが Service Worker の fetch イベント ハンドラ内で処理を開始するタイミングをマークします。現在のページを制御している Service Worker がない場合、これは 0 になります。
  • requestStart は、ブラウザがリクエストを行った時点です。
  • responseStart は、レスポンスの最初のバイトが到着したときです。
  • responseEnd は、レスポンスの最後のバイトが到着したときです。

これらのタイミングにより、Service Worker 内のキャッシュ ルックアップやダウンロード時間など、読み込みパフォーマンスのさまざまな側面を測定できます。

// Cache seek plus response time of the current document
const [pageNav] = performance.getEntriesByType('navigation');
const fetchTime = pageNav.responseEnd - pageNav.fetchStart;

// Service worker time plus response time
let workerTime = 0;

if (pageNav.workerStart > 0) {
  workerTime = pageNav.responseEnd - pageNav.workerStart;
}

リクエスト/レスポンス レイテンシの他の側面も測定できます。

const [pageNav] = performance.getEntriesByType('navigation');

// Request time only (excluding redirects, DNS, and connection/TLS time)
const requestTime = pageNav.responseStart - pageNav.requestStart;

// Response time only (download)
const responseTime = pageNav.responseEnd - pageNav.responseStart;

// Request + response time
const requestResponseTime = pageNav.responseEnd - pageNav.requestStart;

その他の測定方法

Navigation Timing と Resource Timing は、上記の例で概説している以上の用途で役立ちます。その他に、関連するタイミングについて検討する価値のある状況がいくつかあります。

  • ページ リダイレクト: リダイレクトは見落とされがちなレイテンシの原因です。特にリダイレクト チェーンでは注意が必要です。レイテンシは、HTTP から HTTP へのホップや、302/キャッシュされていない 301 リダイレクトなど、いくつかの方法で追加されます。redirectStartredirectEndredirectCount のタイミングは、リダイレクトのレイテンシを評価する際に有用です。
  • ドキュメントのアンロード: unload イベント ハンドラでコードを実行するページでは、ブラウザでそのコードを実行してから次のページに移動する必要があります。unloadEventStartunloadEventEnd はドキュメントのアンロードを測定します。
  • ドキュメントの処理: ウェブサイトから非常に大きな HTML ペイロードが送信されない限り、ドキュメントの処理時間はそれほど問題にならないことがあります。お客様の状況の場合は、domInteractivedomContentLoadedEventStartdomContentLoadedEventEnddomComplete のタイミングが参考になります。

アプリケーション コードでタイミングを取得する

これまでに示した例はすべて performance.getEntriesByType を使用していますが、パフォーマンス エントリ バッファをクエリするには、performance.getEntriesByNameperformance.getEntries など、他の方法を使用します。これらの方法は、軽度分析のみが必要な場合に適しています。ただし、多数のエントリに対して反復処理を行ったり、新しいエントリを見つけるためにパフォーマンス バッファを繰り返しポーリングしたりすることで、メインスレッドの処理が過剰に発生する場合もあります。

パフォーマンス エントリのバッファからエントリを収集するためのおすすめの方法は、PerformanceObserver を使用することです。PerformanceObserver は、パフォーマンス エントリをリッスンし、バッファに追加されるときにそれらを提供します。

// Create the performance observer:
const perfObserver = new PerformanceObserver((observedEntries) => {
  // Get all resource entries collected so far:
  const entries = observedEntries.getEntries();

  // Iterate over entries:
  for (let i = 0; i < entries.length; i++) {
    // Do the work!
  }
});

// Run the observer for Navigation Timing entries:
perfObserver.observe({
  type: 'navigation',
  buffered: true
});

// Run the observer for Resource Timing entries:
perfObserver.observe({
  type: 'resource',
  buffered: true
});

このタイミング収集の方法は、パフォーマンス エントリ バッファに直接アクセスする場合と比べると違和感を覚えるかもしれませんが、ユーザー向けの重要な目的を果たさない作業とメインスレッドを結び付ける方が好ましいでしょう。

自宅への発信

必要な時間データをすべて収集したら、エンドポイントに送信してさらに分析できます。そのためには、navigator.sendBeacon を使用する方法と、keepalive オプションを指定した fetch を使用する方法の 2 つがあります。どちらの方法でも、指定されたエンドポイントにリクエストをブロックせずに送信します。リクエストは、必要に応じて現在のページ セッションよりも長く存続するようにキューに追加されます。

// Caution: If you have lots of performance entries, don't
// do this. This is an example for illustrative purposes.
const data = JSON.stringify(performance.getEntries()));

// The endpoint to transmit the encoded data to
const endpoint = '/analytics';

// Check for fetch keepalive support
if ('keepalive' in Request.prototype) {
  fetch(endpoint, {
    method: 'POST',
    body: data,
    keepalive: true,
    headers: {
      'Content-Type': 'application/json'
    }
  });
} else if ('sendBeacon' in navigator) {
  // Use sendBeacon as a fallback
  navigator.sendBeacon(endpoint, data);
}

この例では、JSON 文字列は POST ペイロードに到着し、必要に応じてデコードしてアプリケーション バックエンドで処理/保存できます。

まとめ

指標を収集したら、そのフィールド データの分析方法を自分で検討する必要があります。フィールド データを分析する場合、有意な結論を導き出すために従うべき一般的なルールがいくつかあります。

  • 平均値は使用しないでください。平均値は個々のユーザーのエクスペリエンスを表したものではなく、外れ値によって偏る可能性があるためです。
  • パーセンタイルに依存します。時間ベースのパフォーマンス指標のデータセットでは、値が小さいほど優れています。つまり、低いパーセンタイルを優先すると、最も高速なエクスペリエンスのみが考慮されるようになります。
  • 値のロングテールを優先します。75 パーセンタイル以上のエクスペリエンスを重視すると、最も遅いエクスペリエンスに集中できます。

このガイドは、ナビゲーションやリソース タイミングについて網羅したリソースではなく、出発点です。以下のリソースもご参照ください。

これらの API と API が提供するデータにより、実際のユーザーによる読み込みパフォーマンスの体験をより深く理解できるようになり、現場での読み込みパフォーマンスの問題をより確実に診断して対処できるようになります。