Amazon Web Services ブログ

First Byte Latency の時間計測と Server-Timing ヘッダーを用いて、ウェブサイトのパフォーマンスのボトルネックを特定する方法

ウェブサイトのパフォーマンス問題はよくあることですが、根本原因の特定は困難な作業となります。この投稿では、 Server-Timing ヘッダーの潜在能力を引き出すことで、パフォーマンスに関するトラブルシューティングのプロセスをシンプルにする方法を学びます。 Server-Timing ヘッダーは、バックエンドのコンポーネントがユーザーリクエストへのレスポンスにおいて、タイミングメトリクスやパフォーマンスモニタリングに関するインサイトを伝達できるようにします。

ウェブサイトのアクセスでは、画像変換などのコンテンツ最適化やデータベースからの動的なデータ取得を含んだ、複雑なサーバーサイドのプロセスが関与しています。遅いリクエストの原因となるサーバーやプロセスを特定するには、複数のログを突き合わせて分析する必要があり、時間がかかってしまいます。このプロセスをシンプルにすることで迅速に問題を解決できます。具体的には、ユーザー体験の品質シグナルとサーバーサイドのパフォーマンス指標とを直接関連付けて、単一のログ行内にカプセル化することで実現します。この方法は、広範なデータクエリや相関分析が不要であり、パフォーマンス問題を素早く特定し、原因となるサーバーコンポーネントまで追跡することを可能にします。このアプローチの実例として Common Media Client Data(CMCD) が挙げられます。 CMCD は動画ストリーミング業界で最近生まれた革新的な技術で、クライアントおよびサーバー両方のオブサーバビリティデータを単一のリクエストログ行にシームレスに統合するものです。ウェブサイトにおいては、 Server-Timing ヘッダーを実装することで同様の方式を採用できます。サーバーサイドのメトリクスとクライアントサイドで利用可能なメトリクスとを効果的に統合し、特定のリクエスト-レスポンスサイクルのパフォーマンスを包括的に把握するのです。

私たちが提案するソリューションは 2 つのパートで構成されます。第一に、エンドユーザーのレイテンシを測定してパフォーマンス問題を特定すること、第二に、そうした問題が発生した際にサーバーのインサイトに即座にアクセスすることです。 まず前者を取り上げてから、 Server-Timing の実装について掘り下げていきましょう。

パフォーマンス問題の検出

ウェブサイトのパフォーマンスはレイテンシに大きく依存します。レイテンシとは、ユーザーアクション(リンクのクリックやフォームの送信など)とサーバーからのレスポンスとの間の時間遅延を指します。ウェブサイトにおけるレイテンシは、通常 Time to First Byte ( TTFB )、別名 First Byte Latency ( FBL )の形式で測定されます。これは、ウェブサイトのコンテンツがユーザーの画面に描画され始めるまでの速さを測定したもので、 First Contentful Paint ( FCP )や Largest Contentful Paint ( LCP )などの Core Web Vitals シグナルに直接影響します。シームレスなユーザー体験を確保するには、 TTFB を 800 ミリ秒以下に維持することが推奨されています。このベンチマークは、遅いリクエストを特定するための有用な閾値として機能します。 Amazon CloudFront のようなサービスを活用することで、静的および動的コンテンツ両方の TTFB の改善に役立ちます。

クライアントサイドの視点で TTFB を測定する際は、ユーザーのリクエスト開始時点から、サーバーからのレスポンスの最初のバイト受信時点までの時間を対象範囲とします。この計算には、ネットワーク伝送時間やサーバーサイドでのすべての処理時間が含まれており、ウェブサイトのアーキテクチャに応じて、コンテンツ配信ネットワーク( CDN )の処理、オリジンサーバーの処理、データベースのクエリ、およびその他のリクエスト処理タスクなどが含まれます。サーバーサイドの視点で TTFB を測定する場合は、 サーバーがリクエストを受信してから、レスポンスの最初のバイトをネットワーク層に送出する時点までの時間を対象範囲とします。このとき、ネットワーク転送時間は含まれず、 TTFB はレスポンスを開始する前のサーバーの処理時間を本質的に示します。さらに、リクエストフローの途中にサーバーが位置するシナリオでは、サーバーは二重の役割を果たします。一つはダウンストリームからのリクエストを受信するサーバーとして、もう一つはアップストリームの他のサーバーにリクエストを転送するクライアントとして機能するのです。この動作モデルは、 Amazon CloudFront のような CDN 内のサーバーにおいて一般的であり、そのようなサーバーではクライアントサイドとサーバーサイドの両方の TTFB メトリクスが存在することになります。

CloudFront とエッジ関数、 Application Load Balancer 、ウェブサーバー、データベースなどのコンポーネントを含む典型的なウェブサイトアーキテクチャでは、リクエストからレスポンスまでのサイクルは図 1 に示すように進行します。

図 1. 典型的なウェブサイトアーキテクチャにおけるリクエスト-レスポンスサイクルのタイミング

図 1. 典型的なウェブサイトアーキテクチャにおけるリクエスト-レスポンスサイクルのタイミング

図 1 では、リクエストとレスポンスの開始と終了のそれぞれのタイムスタンプを T を用いて表しています。これらのタイムスタンプを使用して、様々な TTFB を以下のように計算します:

  • ユーザー TTFB は T1 から T18 までの時間間隔です。ユーザーエクスペリエンスをモニタリングし、推奨値を超えた時の問題特定をするために測定すべき指標です。ユーザー TTFB が短いほど、レスポンスが速く、良いユーザーエクスペリエンスであることを示しています。
  • CloudFront ダウンストリーム TTFB は T2 から T17 までの時間間隔です。キャッシュヒット、つまり、オリジンでの処理を必要とせず CloudFront キャッシュからリクエストが処理される場合には、 TTFB は CloudFront がリクエストを処理してレスポンスを準備するのにかかった時間のみを示します。エッジ関数を使用するのであれば、その実行時間も含まれます。ただし、キャッシュミスの場合には、オリジンがリクエストを処理してレスポンスを準備するまでにかかった時間と、オリジンから CloudFront へレスポンスを転送する時間が追加されます。
  • CloudFront アップストリーム TTFB は T3 から T14 までの時間間隔です。これは、CloudFront がリクエストをオリジンに送信し、レスポンスを受信するまでのキャッシュミスの場合を表しています。
  • CloudFront と同様に、オリジン側のシステム内のすべてのサーバーも独自の TTFB を持っています。たとえば、HTML ページを生成するためにデータベースクエリを実行する場合に、T7 から T10 までの時間間隔として、データベース処理時間と伝送時間の両方を測定します。
  • アップストリームのコンポーネントからダウンストリームへの伝送時間は、ダウンストリーム TTFB からアップストリーム TTFB を引いた値で推定できます。たとえば、CloudFront からユーザーへの最初のバイトの伝送時間は、ユーザー TTFB から CloudFront ダウンストリーム TTFB を引いて計算できます。伝送時間が短いほど、ネットワーク状態が良好で、距離が短いことを示します。

ブラウザ内の JavaScript を使用してユーザー TTFB を測定するには、Resource Timing API が使用できます。この API では、リクエストの開始時刻、DNS 解決時間、TCP および TLS ハンドシェイク、レスポンスの最初のバイトの受信といったリソースの読み込みに関わるさまざまな段階のタイムスタンプを取得できます。これにより、TTFB の計算やリソースの読み込みに関連するその他の有用なタイミング情報の取得が容易になります。

const timings = {};

new PerformanceObserver((entryList) => {
  const entries = entryList.getEntries();
  
  entries.forEach(entry => {
    if (entry.responseStart > 0) {
      timings.userDNS = (entry.domainLookupEnd - entry.domainLookupStart).toFixed(2);
      timings.userTCP = (entry.connectEnd - entry.connectStart).toFixed(2);
      timings.userTLS = (entry.requestStart - entry.secureConnectionStart).toFixed(2);
      timings.userTTFB = (entry.responseStart - entry.requestStart).toFixed(2);
    }
  });
}).observe({
  type: 'resource',
  buffered: true
});

このコードスニペットは、ウェブページから読み込まれた各リソースの DNS、TCP、TLS、および TTFB のタイミングを取得しています。同様に、Navigation Timing API を使用して、ブラウザ内のナビゲーションリクエストに対してこれらのタイミングを取得できます。このデータを使用すると、レイテンシが許容範囲内かどうかを判断できるだけでなく、リクエストの DNS、TCP、TLS 各段階の所要時間を分析することもできます。これらのメトリクスは、パケットが移動することになるユーザーとフロントエンドサーバー間のネットワーク距離や、ネットワークの輻輳状態に影響を受けます。これらの値が大きく、800 ミリ秒のベンチマークに近づいている場合は、よりスムーズなユーザーエクスペリエンスのためにネットワーク状態を改善する必要があることを示しています。 CloudFront は、エッジロケーションでユーザーに近い場所でリクエストを終端することにより、ネットワークパフォーマンスを大幅に向上させることができます。

しかし、サーバーサイドが原因のパフォーマンス問題はこのデータでは可視化できません。そこで Server-Timing が役立ちます。

Server-Timing の実装

あらゆるウェブサーバーでは HTTP レスポンスに Server-Timing ヘッダーを含めることができ、サーバーメトリクスを提供します。このヘッダーはすべてのモダンブラウザでサポートされており、PerformanceServerTiming インターフェースを使用してメトリクスを簡単に解析および取得できます。 CloudFront はすでに Server-Timing をサポートしており、処理に関連するメトリクスを伝達できます。たとえば、cdn-downstream-fbl メトリクスは前述した CloudFront ダウンストリーム TTFB であり、 cdn-upstream-fbl は CloudFront アップストリーム TTFB です。その他の利用可能なメトリクスとその説明については、開発者ガイドで確認できます。

CloudFront で Server-Timing を有効化するには、レスポンスヘッダーポリシーを作成します。 Server-Timing ヘッダーパネルの「有効」オプションを切り替え、サンプリングレートを指定します。他のレスポンスヘッダーの追加または削除も必要に応じて設定します。CloudFront の Server-Timing 機能により、CloudFront がリクエストを十分な速さで処理できているかを評価できます。キャッシュヒットの場合、 cdn-downstream-fbl メトリクスの値は小さくなり、これは CloudFront が迅速にレスポンスを開始したことを示します。逆に、このメトリクスの値が大きい場合は、処理が遅いことを示唆し、CloudFront 側に問題があることを示します。キャッシュミスの場合には、 cdn-upstream-connect と cdn-upstream-dns メトリクスを確認して CloudFront からオリジンへの接続時間の値も評価します。これらのメトリクスの値が小さい場合は、リクエストフローにおける次のサーバー(図 1 に示されている Application Load Balancer など)が正常に稼働しており、接続を素早く確立し、CloudFront のオリジン向けサーバーの近くに配置されていることを示唆しています。たいていの場合、 cdn-upstream-connect や cdn-upstream-dns の値は 0 になります。なぜならば、CloudFront の持続的接続機能が以前に確立された接続を再利用しているからです。 cdn-upstream-fbl メトリクスは、オリジンからのレスポンスの最初のバイトが CloudFront に到達する速さを示します。このメトリクスの値が大きく、 cdn-upstream-connect と cdn-upstream-dns の値が小さい場合は、 Application Load Balancer の後段にあるオリジン側のシステムに問題が発生し、速いレスポンスを提供できていないことを示します。理想的には、これらのメトリクスがユーザーの経験するレイテンシ(ユーザー TTFB )に大きく影響を与えてはいけません。

CloudFront の Server-Timing ヘッダーは、 CloudFront のダウンストリーム・アップストリーム両方のパフォーマンスに関するインサイトを提供しますが、リクエスト中にオリジンで何が起こったかを直接教えてはくれません。複数の異なるコンポーネントやテクノロジーで構成される現代のオリジンアーキテクチャの多様性を鑑みれば、包括的な理解のためには、それぞれのパフォーマンスタイミング情報を組み込むことが不可欠です。オリジンサーバーからインサイトを抽出するには、オリジンからの CloudFront へのレスポンスに Server-Timing ヘッダーを独自に実装して含めることができます。 CloudFront がこのヘッダーを置き換えることはありません。代わりに、オリジンから受信した Server-Timing ヘッダーに CloudFront が自身のメトリクスを追加します。独自で実装する Server-Timing ヘッダーに含めるタイミングメトリクスとしては、画像の最適化、 API 呼び出し、データベースのクエリ、エッジコンピューティングなどの重要なバックエンドプロセスの測定値が考えられます。たとえば、 PHP を使用してデータベースクエリを実行している場合、次のようにしてクエリの所要時間を測定できます。

$dbReadStartTime = hrtime(true);
// Database query goes here
$dbReadEndTime = hrtime(true);
$dbReadTotalTime = ($dbReadEndTime - $dbReadStartTime) / 1000000;
header('Server-Timing: my-query;dur=' . $dbReadTotalTime);

こちらのコードスニペットでは、データベース操作の完了にかかった時間を取得し、 Server-Timing ヘッダー内で my-query メトリクスとして伝達しています。データベースは時に過負荷状態になり、パフォーマンスのボトルネックとなることがあるため、このデータはそのようなシナリオを明らかにするのに役立ちます。

Node.js を使用している場合は、 PerformanceServerTiming インターフェース仕様のを参考にして、 Server-Timing ヘッダーを実装してください。

エッジ関数によって追加になるレイテンシも Node.js を使用している場合と同様の実装で測定を行います。ネットワーク呼び出しを含んだ複雑な処理を行う Lambda@Edge 関数では特に有益です。以下の例では、オリジンレスポンスイベントにアタッチされた Lambda@Edge 関数で Server-Timing ヘッダーの実装をしています:

import json
import time

# CF headers are available in request object for Lambda@Edge functions attached to origin response event only

def lambda_handler(event, context):

    # Get function's start timestamp
    handler_start_time = time.time()

    response = event['Records'][0]['cf']['response']
    request = event['Records'][0]['cf']['request']
    server_timing_value = []

    # List of CloudFront headers to include in server timing for additional inisghts
    cf_headers = ['cloudfront-viewer-country', 'cloudfront-viewer-city',
                  'cloudfront-viewer-asn']

    # Iterate over each header name and construct the value for the Server-Timing header
    for header_name in cf_headers:
        if header_name in request['headers']:
            header_value = request['headers'][header_name][0]['value']
            server_timing_value.append('{}; desc="{}"'.format(header_name, header_value))

    # Function's logic goes here

    # Get function's stop timestamp
    handler_stop_time = time.time()
    handler_duration = round((handler_stop_time - handler_start_time) * 1000, 2)
    server_timing_value.append('{}; dur={}'.format("my-function", handler_duration))

    if server_timing_value:
        # Construct the Server-Timing header
        server_timing = [{
            "key": "Server-Timing",
            "value": ', '.join(server_timing_value)
        }]

        # Add or append the Server-Timing header
        if 'server-timing' in response['headers']:
            response['headers']['server-timing'][0]['value'] += ', ' + ', '.join(server_timing_value)
        else:
            response['headers']['server-timing'] = server_timing

        print("Server-Timing:", response['headers']['server-timing'])

    return response

注目すべき点として、このコードで追加されるメトリクスは、ハンドラーコードの実行時間のみであり、その他の Lambda のタイミングは除外されています。なお、オリジンアーキテクチャの情報が悪意のある攻撃者に悪用されるおそれがあるため、メトリクスには意図的に抽象的な名前を使用しています。 また、ユーザーの地理的位置情報と ASN 番号に関するインサイトを Server-Timing ヘッダーに追加したことにも注目すべき点です。 そして、 クライアントサイドでも Server Timing を取得できるように serverTiming プロパティを使用してコードを拡張します。以下が修正されたコードスニペットとなります。

// Creating a new PerformanceObserver to monitor performance entries
new PerformanceObserver((entryList) => {
  const entries = entryList.getEntries();

  for (const entry of entries) {
    // Object to store timings for various stages
    const timings = {
      userDNS: null,                // User DNS resolution time
      userTCP: null,                // User TCP handshake time
      userTLS: null,                // User TLS handshake time
      CFDNS: null,                  // CDN DNS resolution time
      CFUpstreamHandshake: null,    // CDN upstream TCP handshake time
      MyQuery: null,                // Query time
      CFUpstreamTTFB: null,         // CDN upstream Time To First Byte (TTFB)
      MyFunction: null,             // Function execution time
      CFDownstreamTTFB: null,       // CDN downstream TTFB
      userTTFB: null,               // User Time To First Byte (TTFB)
      CFRID: null,                  // CDN Request ID
      CFCacheStatus: null,          // CDN Cache status (Hit or Miss)
      UserASN: null                // User Autonomous System Number (ASN)
    };

    // Iterating through server timing entries for the current performance entry
    entry.serverTiming.forEach((serverEntry) => {
      switch (serverEntry.name) {
        case 'cdn-rid':
          timings.CFRID = serverEntry.description;
          break;
        case 'cdn-cache-miss':
          timings.CFCacheStatus = "Miss";
          break;
        case 'cdn-cache-hit':
          timings.CFCacheStatus = "Hit";
          break;
        case 'cdn-upstream-connect':
          timings.CFUpstreamHandshake = serverEntry.duration;
          break;
        case 'cdn-downstream-fbl':
          timings.CFDownstreamTTFB = serverEntry.duration;
          break;
        case 'cdn-upstream-dns':
          timings.CFDNS = serverEntry.duration;
          break;
        case 'cdn-upstream-fbl':
          timings.CFUpstreamTTFB = serverEntry.duration;
          break;
        case 'my-query':
          timings.MyQuery = serverEntry.duration;
          break;
        case 'my-function':
          timings.MyFunction = serverEntry.duration;
          break;
        case 'cloudfront-viewer-asn':
          timings.UserASN = serverEntry.description;
          break;
      }
    });

    // Calculating user-specific timings if the response not served from the local cache
    if (entry.responseStart > 0) {
      timings.userDNS = (entry.domainLookupEnd - entry.domainLookupStart).toFixed(2);
      timings.userTCP = (entry.connectEnd - entry.connectStart).toFixed(2);
      timings.userTLS = (entry.requestStart - entry.secureConnectionStart).toFixed(2);
      timings.userTTFB = (entry.responseStart - entry.requestStart).toFixed(2);

      // Logging metrics for the current entry
      console.log("Metrics for:", entry.name);
      console.log("userDNS:", timings.userDNS);
      console.log("userTCP:", timings.userTCP);
      console.log("userTLS:", timings.userTLS);
      console.log("CFDNS:", timings.CFDNS);
      console.log("CFUpstreamHandshake:", timings.CFUpstreamHandshake);
      console.log("DBQuery:", timings.MyQuery);
      console.log("CFUpstreamTTFB:", timings.CFUpstreamTTFB);
      console.log("lambdaEdge:", timings.MyFunction);
      console.log("CFDownstreamTTFB:", timings.CFDownstreamTTFB);
      console.log("userTTFB:", timings.userTTFB);
      console.log("CFRID:", timings.CFRID);
      console.log("CFCacheStatus:", timings.CFCacheStatus);
      console.log("UserASN:", timings.UserASN);
      console.log("------------------------------------------------------");
    }
  }
}).observe({
  type: 'resource',   // Observing resource-related performance entries
  buffered: true
});

この改良されたコードスニペットでは、Server Timing を取得したのちに、クライアントメトリクスと共に timings オブジェクトに統合しています。これにより、クライアントサイドとサーバーサイドの両方のリクエスト – レスポンスサイクルにおける包括的なパフォーマンスのインサイトを一箇所にまとめることができました。以下は console.log の出力例です。

Metrics for: https://d1234.cloudfront.net/script.php
userDNS: 0.00
userTCP: 0.00
userTLS: 5.00
CFDNS: 0
CFUpstreamHandshake: 88
DBQuery: 0.538685
CFUpstreamTTFB: 178
lambdaEdge: 0.09
CFDownstreamTTFB: 229
userTTFB: 233.10
CFRID: mRq-Uvr__3OBDo0IX9ELV5Lrk3lF-bOp4eOIqTEXlFkFn0wIWPKgpA== 
CFCacheStatus: Miss
UserASN: 1257

この例を見てみましょう。 CloudFront の Lambda@Edge 関数の実行と、オリジンサーバーのデータベースクエリの両方を合わせて、最初のバイトをネットワークに送信するまでに 229 ミリ秒かかりました( CFDownstreamTTFB )。最初のバイトは 233 ミリ秒後にクライアントデバイスに到達しているので( userTTFB )、伝送時間は 4 ミリ秒であることを示しています。クライアントデバイスは、以前に確立された TCP および TLS 接続を再利用しており( userTCP 、 userTLS )、 CloudFront の IP アドレスをキャッシュしていました( userDNS )。 CloudFront はオリジンに向けて新しい TCP 接続を確立する際( CFUpstreamHandshake )、 88 ミリ秒かかりました。オリジンはリクエストを 90 ミリ秒以内( CFUpstreamTTFB – CFUpstreamHandshake )で処理しており、素早くレスポンスの最初のバイトを返していることがわかります。結論として、エンドユーザーの全体的なレイテンシは推奨値の 800 ミリ秒を下回っており、満足のいくものであると言えます。

Server-Timing を他のデータで拡充する

Server-Timing ヘッダーは、サーバーサイドの処理時間を伝達するために設計されたものですが、その構文は単にその用途に限定されたものではありません。たとえば、CloudFront ではキャッシュのステータスや内部のユニークなリクエスト ID をメトリクスに含んでいます。これらのデータは CloudFront の処理を正確に分析するために不可欠なものです。同様にして、リクエストの経路に関してインサイトを提供するメトリクスを加えて、 Server-Timing ヘッダーを独自に拡充することができます。たとえば、ログを見つけやすくするために、クラスター内のサーバーの内部 ID を追加することもできます。ユーザーの地理的位置情報やデバイスタイプも追加はできますが、 CloudFront のヘッダーの使用で実現できます。先述の Lambda@Edge 関数で使用方法を示した通りです。これらのヘッダーは、オリジンレスポンスイベントに関連付けられた Lambda@Edge 関数、もしくは、ビューワーレスポンスイベントやビューワーリクエストイベントに関連付けられた CloudFront 関数のリクエストオブジェクトで利用できます。オリジンリクエストポリシーでこれらのヘッダーを有効化すると、 オリジンウェブサーバーが CloudFront からのリクエストに含まれるこれらのヘッダーを取り扱えるようになります。こうして、拡充されたメトリクスを Server-Timing ヘッダーに統合するのです。

Amazon CloudWatch で結果を分析する

Server-Timing ヘッダーは、パフォーマンス問題を特定し、根本原因を突き止めるのに有用ですが、ウェブサイトパフォーマンスの他の重要な側面に関するインサイトは提供しません。たとえば、JavaScript 実行に関連するエラーや、累積レイアウトシフト( Cumulative Layout Shift )などの特定の Web Vitals メトリクスは、このソリューションでは直接キャプチャされません。もしすでにリアルユーザーモニタリング( RUM ) ベースのウェブサイトモニタリングソリューションを利用しているのであれば、 Server-Timing ヘッダーを統合することで、 既存の手法を置き換えたり、パフォーマンスモニタリングを Server-Timing のみに限定したりするのではなく、既存の手法を補完できます。包括的なウェブサイトモニタリングソリューションの一例として Amazon CloudWatch RUM があります。

CloudWatch RUM のインサイトを前述の手法で拡張するには、 Server-Timing ヘッダーから抽出したメトリクスを取得するカスタムイベントを作成し、 CloudWatch RUM クライアント経由で CloudWatch に送信します。このアプローチにより、すべての CloudWatch RUM のインサイトと Server-Timing を同じサービス内に統合し、両方のデータセットをシームレスに分析できるようになります。

前述のコードスニペットについて、 Server-Timing ヘッダーから抽出したデータとクライアントサイドの測定値を使用して、CloudWatch RUM クライアントを介してカスタムイベントを記録する方法の例を以下に示します:

// Sending performance data to a remote server
cwr('recordEvent', {
    type: 'my-server-timing',
    data: {
        current_url: entry.name,
        ...timings  // Spread operator to include all timings
    }
});

この例では、 timings オブジェクトのすべてのプロパティと値を、cwr 関数に送信される data オブジェクトに含めています。これは、特定のエントリに対してキャプチャされたすべてのタイミングが、 current_url と共に送信されることを意味します。

カスタムイベントは CloudWatch Logs に記録され、CloudWatch Logs Insights を使用してクエリを実行できます。さらに、メトリクスフィルターを使用して CloudWatch メトリクスを作成して、モニタリング目的のメトリクスアラームを設定することができます。

上記のコードで収集しているタイミングのカスタムメトリクス実装の例を以下に示します:

{
  "event_timestamp": 1710929230000,
  "event_type": "my-server-timing",
  "event_id": "9ae82980-4bfb-47f5-8183-b241379e09e1",
  "event_version": "1.0.0",
  "log_stream": "2024-03-20T03",
  "application_id": "c27d1cef-e531-45ad-9bc4-8e03a716c775",
  "application_version": "1.0.0",
  "metadata": {
    "version": "1.0.0",
    "browserLanguage": "en",
    "browserName": "Chrome",
    "browserVersion": "123.0.0.0",
    "osName": "Mac OS",
    "osVersion": "10.15.7",
    "deviceType": "desktop",
    "platformType": "web",
    "pageId": "/",
    "interaction": 0,
    "title": "TTFB Demo",
    "domain": "d1234.cloudfront.net",
    "aws:client": "arw-script",
    "aws:clientVersion": "1.17.0",
    "countryCode": "SE",
    "subdivisionCode": "AB"
  },
  "user_details": {
    "sessionId": "c9d2514a-8884-4b32-aec0-25203f213f84",
    "userId": "0f7f2bf3-c9b7-46ab-bc9e-2ff53864ea74"
  },
  "event_details": {
    "current_url": "https://d1234.cloudfront.net/getmeal.php",
    "userDNS": "0.00",
    "userTCP": "0.00",
    "userTLS": "9.50",
    "CFDNS": 0,
    "CFUpstreamHandshake": 90,
    "MyQuery": 0.517874,
    "CFUpstreamTTFB": 180,
    "MyFunction": 0.12,
    "CFDownstreamTTFB": 233,
    "userTTFB": "239.30",
    "CFRID": "ujYncZYVJeIOk6fI7ApFuNt-mJoh8hfL3nZPgAj77z7RdtSzNMTcqQ==",
    "CFCacheStatus": "Miss",
    "UserASN": "1257"
  }
}

このメトリクスに基づいて、ユーザー TTFB のメトリクス用に以下のフィルターパターンを作成できます:

{ $.event_details.userTTFB= * && $.event_details.CFCacheStatus= * && 
$.event_details.UserASN= * && $.metadata.countryCode=*}

これにより、国コード、 ASN 番号、 CloudFront キャッシュステータスなどのディメンションを持つユーザー TTFB のメトリクスを作成できます。その後、このメトリクスに対してアラートを作成し、推奨される 800 ミリ秒などの事前定義された静的な閾値を超えた場合に通知を受け取ることができます。また、CloudWatch 異常検出 を利用することもできます。

最適化とコスト

重要なこととして、Server-Timing ヘッダーがレスポンスサイズを増加させる点を認識してください。これは、 CloudFront のデータ転送アウトに関するコストや、分析システム内でのデータの保存や処理に影響を与える可能性があります。たとえば、前述の Server-Timing ヘッダーの値は約 350 バイトに相当しますが、仮に 100 万リクエストを仮定した場合、追加で 0.325 ギガバイトのデータを転送することになります。ウェブサイトが受信するリクエスト数によっては、これが大きなコストになる場合とならない場合があります。ただし、 Server-Timing に必須情報、特にアクション可能なデータのみを含めることで、このコストを削減できます。たとえば、主にパフォーマンス低下の検出に Server-Timing が必要な場合は、推奨しきい値である 800 ミリ秒を超えるリクエストにのみ追加することを選択できます。さらに、インタラクティブフォームや API 呼び出しなど、ウェブサイト上の重要なリソースの読み込みにのみ適用することで、使用量を最小限に抑えることもできます。これには、クライアントサイドの JavaScript コードで該当するメトリクスに必要なフィルターを実装することで実現できます。

まとめ

この投稿では、ウェブサイトパフォーマンスモニタリングにおける TTFB の重要性を探求し、リクエスト-レスポンスサイクルにおいて詳細なインサイトを提供する Server-Timing ヘッダーの活用方法を実証しました。レイテンシの測定とサーバーサイドメトリクス( CloudFront の処理時間、オリジンサーバーのレスポンス時間など)の取得により、ウェブサイトの所有者はパフォーマンス問題の根本原因を特定し、ウェブサイトの最適化に向けた積極的な対策を講じることができます。

本記事は「 How to identify website performance bottlenecks by measuring time to first byte latency and using Server-Timing header 」と題された記事の翻訳となります。 翻訳はプロフェッショナルサービスの 鈴木(隆) が担当しました。