Navigation Timing と Resource Timing を使用して実際の読み込みのパフォーマンスを評価する方法

Navigation Timing API と Resource Timing API を使用して、実際の環境での読み込みパフォーマンスを評価する基本について説明します。

公開日: 2021 年 10 月 8 日

ブラウザのデベロッパー ツールのネットワーク パネル(または Chrome の Lighthouse)で接続スロットリングを使用して読み込みパフォーマンスを評価したことがある方は、これらのツールがパフォーマンス チューニングにどれほど便利かをご存じでしょう。一貫した安定したベースライン接続速度で、パフォーマンスの最適化の効果をすばやく測定できます。唯一の問題は、これが合成テストであり、ラボデータではなく、実際の環境データが得られることです。

合成テストは本質的に悪いものではありませんが、実際のユーザーにとってウェブサイトの読み込みがどれほど速いかを把握することはできません。そのためには、Navigation Timing API と Resource Timing API から収集できる実際の環境データが必要です。

実際の環境での読み込みパフォーマンスの評価に役立つ API

Navigation Timing と Resource Timing は、2 つの異なるものを測定する、類似した API です。

  • Navigation Timing は、HTML ドキュメントのリクエスト(つまりナビゲーション リクエスト)の速度を測定します。
  • Resource Timing は、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 の [ネットワーク パネル]でのネットワーク リクエストの可視化

ネットワーク リクエストのライフサイクルには、DNS ルックアップ、接続の確立、TLS ネゴシエーションなど、レイテンシの発生源となる明確なフェーズがあります。これらのタイミングは DOMHighResTimestamp として表されます。ブラウザによっては、タイミングの粒度がマイクロ秒単位になることも、ミリ秒単位に切り上げられることもあります。これらのフェーズと、Navigation Timing と Resource Timing との関係を詳しく調べる必要があります。

DNS ルックアップ

ユーザーが URL にアクセスすると、ドメイン名システム(DNS)にクエリが送信され、ドメインが IP アドレスに変換されます。このプロセスには時間がかかる場合があります。実際の環境で測定することをおすすめします。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 を選択する以外は、ユーザーはどこからでもウェブにアクセスできるため、ほとんど制御できません。
  • 内部要因: サーバーとクライアントサイドのアーキテクチャ、リソースサイズ、それらの最適化機能など、制御可能なものです。

どちらの要因も読み込みパフォーマンスに影響します。これらの要因に関連するタイミングは、リソースのダウンロードにかかる時間を表すため、非常に重要です。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 から HTTPS へのホップや、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 または fetchkeepalive オプション を設定して使用します。どちらの方法でも、指定したエンドポイントにリクエストがノンブロッキングで送信され、必要に応じて現在のページ セッションよりも長くキューに登録されます。

// Check for navigator.sendBeacon support:
if ('sendBeacon' in navigator) {
  // 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());

  // Send the data!
  navigator.sendBeacon('/analytics', data);
}

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

まとめ

指標を収集したら、その実際の環境データを分析する方法を考える必要があります。実際の環境データを分析する際には、意味のある結論を導き出すために、次の一般的なルールに従う必要があります。

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

このガイドは、Navigation Timing と Resource Timing に関する包括的なリソースではなく、出発点となることを目的としています。その他の参考資料を以下に示します。

これらの API とそれらが提供するデータを使用すると、実際のユーザーが読み込みパフォーマンスをどのように体験しているかをより深く理解できるようになり、実際の環境での読み込みパフォーマンスの問題の診断と解決に自信を持って取り組めるようになります。