私たちは通常、ブラウザ上に表示されるものを見始めた時や、コンテンツを消費したりページと対話したりできるようになった時に、何が起こるかを測定するという文脈で、Webパフォーマンスについて話します。開発者として、フロントエンド開発者として、Webパフォーマンスは大変重要だからです。例えば、以下のCore Web Vitalsは『私たちが見ることができるもの』、『使用することができるもの』、『経験することができるもの』についての議論を導くものです。
- First Contentful Paint(FCP):ユーザーが最初にページを開いてから、コンテンツの一部がレンダリングされるまでの時間のこと
- Largest Contentful Paint(LCP):ページのロードタイムラインにおいて、ページのメインコンテンツがロードされるまでの時間のこと
- Interaction to Next Paint(INP):Webページがユーザーの入力にどれだけ速く反応するかを測定する
しかし、Webページの最初のバイト(Byte: データのごく小さい単位)がブラウザに受信される前に起こるイベントについてはどうでしょうか?
そのようなイベントを測定し、最適化することで、Webページやアプリケーションの読み込みをさらに速くすることは可能でしょうか?
Sentryトレースビューを使い、TTFB前のイベントがどのように可視化されるのか
Sentryのトレースビューを確認すると、browserウィンドウで何かがレンダリングされる前に起こるイベントがキャプチャされ、[browser]のスパンとしてラベル付けされていることがわかります。キャッシュ、DNS、接続、TLS/SSL、リクエスト、レスポンスの6つのスパンが時系列で登録されています。レスポンス以前のすべてのイベントは、Webページ/リソースへのリクエストからレスポンスの最初のバイトが到着し始めるまでの時間を計測するTTFB(Time to First Byte)に先行します。
Sentryがブラウザで初期化されていないにも関わらず、これらのイベントがどのようにしてSentryが捕らえているのか不思議に思うかもしれません。その答えは、Performance API(Webアプリケーションのパフォーマンスを測定するために使用されるウェブ標準のグループ)にあります。より具体的にいうと、Navigation Timing APIにあります。
本当に素晴らしい点は、ブラウザでパフォーマンス API に直接アクセスできることです。パフォーマンス・エントリーのほとんどは、どのWebページに対しても記録されており、それらを取得するためのセットアップや余分なコードは必要ありません。
開発ツールのコンソールを開き、window.performanceと入力して試してみてください。以下のようなものが表示されます。
※解析しやすいように、関連するタイムスタンプを手作業でグループ化し、順番に並べています。
それでは、Sentryはどのようにしてブラウザのスパンを読み取っているのでしょうか?パフォーマンスAPIが、URLがブラウザでリクエストされた瞬間からタイムスタンプでこれらのメトリクスを記録する結果、Sentry JavaScript SDKは、初期化後にこれらにアクセスし、Webページがロードされる前に、時系列的に起こったイベントの完全なリストを埋め戻し、トレースビューで可視化できるように、関連する完全なトレースにスパンとして送信することができます。
Webページが読み込まれる前に起きていること
window.performanceは、Webページのコンテンツがブラウザに表示されるまでに起こるさまざまなイベントへのウィンドウを提供します。
これはPerformanceオブジェクトを返し、そのオブジェクトは上記のコード例にあるtimingpropertyを含んでいます。これはコードを書かずにページ読み込み時にブラウザによって記録されたイベントを検査する素早い方法ですが、Performance.timingプロパティは現在では非推奨となっており、PerformanceNavigationTiming APIに取って代わりました(これまでのところ、わずかな変更のみです)。
Navigation Timing Level 2仕様のこの図は、ブラウザでナビゲーション要求が行われた瞬間から、現在のドキュメントのロードイベントが完了するまでPerformanceNavigationTimingイベントが記録される順序を示しています。すべてのイベントが各ページのロードで利用できるわけではありませんが、順序は上記のwindow.performanceを使用して観察したものと一致しています!
それでは次に、関連する各イベントメトリックを調べてみましょう。
一体、ボンネットの下で何が起こっているのか、そして、トレースビューのブラウザスパンを入力するために、特定のタイムスタンプから Sentry によってどのように計算されるのかを見てみます。そして、この新しい知識を得ることで、TTFBの前にWebパフォーマンスを最適化できるかもしれません。
ブラウザキャッシュ
リソースがHTTP GETを使用してフェッチされる場合(例えば、Webページへの標準的なリクエスト)、ブラウザは最初にHTTPキャッシュをチェックします。fetchStartは、ブラウザがキャッシュのチェックを開始する直前の時刻を返します。Sentry Trace Viewのキャッシュスパンは、fetchStartタイムスタンプとdomainLookupStartタイムスタンプの間の時間として計算されます。
トレースビューのキャッシュスパンのゼロでない値は、ブラウザがブラウザキャッシュからリソースを取得するのにかかった時間を表します。キャッシュスパンが長い場合、遅いブラウザや古いブラウザを使用しているか、ブラウザのキャッシュを頻繁にクリアしないユーザである可能性が高いといえます。
ブラウザDNS
次のスパンは、DNS(ドメインネームシステム)のルックアップ時間(ドメイン名をIPアドレスに変換したり、IPアドレスをドメイン名に変換すること)を報告しています。ユーザーがURLをリクエストすると、DNSはデータベース内のドメインを「検索」し、IPアドレスに変換するために問い合わせを行います。これを完了するのにかかった合計時間は、domainLookupEndタイムスタンプ値からdomainLookupStartタイムスタンプ値を引くことで計算されます。
ブラウザ接続
次に、ブラウザがWebサーバに接続するまでの時間を測定します。これは「コネクション・ネゴシエーション」と呼ばれ、connectStart(ブラウザがWebサーバーへの接続を開始するとき)とconnectEnd(Webサーバーへの接続が確立されたとき)の2つのイベント間の時間として測定されます。
ブラウザ TLS/SSL
ブラウザが接続しているWebサーバがHTTPSを使用している場合、connectStartとconnectEndの間にsecureConnectionStartイベントが発生します。
secureConnectionStartは、ブラウザとWebサーバがTLS(Transport Layer Security)ネゴシエーションとして知られる安全な暗号化接続を確認し、検証するためにメッセージを交換するタイミングを示します。secureConnectionStartの値は、HTTPSが使用されていない場合、または接続が永続的な場合は0になります。
Sentry では、接続イベントと TLS イベントは別々のスパンとして報告されます。
このトレースビューの画像では、connect イベントが始まり、その直後に TLS イベントが始まり、TLS ネゴシエーションが終了すると同時に connect end イベントが終了していることがわかります。
イベントのこの表示は、Webサーバ接続または TLS ネゴシエーションのいずれかにボトルネックがあるかどうかを個別に特定するのに便利です。
ブラウザリクエスト
Webサーバーとの安全な接続が確立された後、ブラウザはrequestStartイベントで示されるリソースへのリクエストを正式に行います。
ブラウザレスポンス
最後に、ブラウザはコンテンツの最初のバイト(Byte)を受け取ります。
Sentry Trace Viewでは、ここにTTFB(Time to First Byte)の縦線が表示されます。
PerformanceNavigationTimingイベントをさらに速くできるか?
ブラウザのキャッシュ取得イベントを高速化できるのか?
自分のアプリケーションのパフォーマンスを向上させたい開発者として、ユーザーベースのためにこのイベントをスピードアップできるかどうかはわかりません。しかし、個人的なブラウザのキャッシュに注意することで、このイベントを高速化することはできるでしょう。gitリポジトリに変更をコミットするように、ブラウザのキャッシュをクリアします。
DNS検索を高速化できますか?
DNSルックアップの速度は、以下を含む多くの事柄によって影響を受ける可能性があります。※ただし、これらに限定されません。
- DNSプロバイダーのインフラの規模:世界中にある「POP(Point of Presence)」の数が少ないと、待ち時間が長くなり、ルックアップに時間がかかります。
- POPの位置:Webサイトの訪問者がDNSサーバーから遠く離れている場合、DNSルックアップに時間がかかります。
- DNSキャッシュ時間: DNSは有効期限が切れるまでキャッシュから提供されます。DNSキャッシュ時間の長さは、DNSレコード(URLをIPアドレスに指す)に指定されたTTL(Time to Live)値によって決まります。TTLが高ければ高いほど、ブラウザはその後のリクエストごとに別のDNSルックアップを実行する必要がなくなります。
結局のところ、DNSルックアップを高速化するには、大規模かつグローバルに分散したPOPネットワークを持つDNSプロバイダーに投資する必要があります。あなたが大規模なエンタープライズ・ビジネスの開発者であれば、おそらくこのようなことはお手の物でしょう。さらに、頻繁に変更されないDNSレコードのTTL値をできるだけ高く設定することは、おそらくベストプラクティスになると思います。
この記事を書いている時、興味本位で自分個人のWebサイトのDNSレコードをチェックしてみたところ、TTLを5分に設定していました。
これは、DNSキャッシュが5分ごとに失効することを意味し、ブラウザが必要以上に頻繁に新しいDNSルックアップを行う原因となっていたのです。
自分のWebサイトのURLを新しいサーバーに向けることはないので、TTLを60分に変更することにしました。
私の個人的なWebサイトで、5日間限定の実験を行いました。
その結果、TTLを切り替えてから、SentryのブラウザのDNSルックアップのスパンでゼロ以外の時間が少なくなりました。もしあなたのWebサイトがミッションクリティカルでなく、お金にならないものであれば、これはDNSルックアップのスピードアップに役立つ良いソリューションかもしれません。
しかし、メインサーバーがダウンし、バックアップサーバーにURLを指定したい場合、世界中のすべてのユーザーがDNSの変更を確認するのに最大60分かかることを頭に置いておいてください。とはいえ、Sematextによると、「平均的なDNSルックアップ時間は20~120ミリ秒である。この間かそれ以下であれば、一般的に非常に優れていると考えられている」とのことです。
したがって、おそらくこの種のマイクロ最適化は、プライマリサーバーが停止している間にバックアップサーバーに変更する必要がある場合に、TTLが60分に設定されていることを覚えておく必要はないかもしれません。
[rel=”dns-prefetch”]によるサードパーティリソースのDNSルックアップの改善
ほとんどのWebサイトやアプリは、サードパーティから少なくとも1つ以上のリソース、つまり異なるドメインのリソース/画像/ファイル/スクリプトをロードしていると思われます。異なるドメインへの各リクエストには、DNSルックアップのイベントも含まれます。サードパーティのリソースはTTFBの後にリクエストされるため、この記事で扱うPerformanceNavigationTimelineイベントの後にリクエストされることは注目に値します。しかし、リソースをリクエストする<link>タグに [rel=”dns-prefetch”] 属性と関連するhref値を使用することで、サードパーティのリソースのDNSルックアップを高速化することが可能になります。
これにより、ユーザがリソースのオリジンから取得する必要がある可能性が高いというヒントをブラウザに提供し、その時点でブラウザは、リソースが正式にリクエストされる前にそのオリジンのDNS解決を先行して実行することで、ユーザ体験を改善する試みが実行できます。これは、例えばGoogleからサードパーティのフォントを取り込む際に便利です。
ページロード時にどれだけの数のサードパーティリソースが並行してリクエストされるかによりますが、responseEndイベントの後、つまりブラウザがHTMLの解析を開始し、すべてのサードパーティリソース(特にレンダーブロッキングリソースの場合)をリクエストするときに発生するブラウザ内イベントを高速化するのに役立つ可能性があります。
※Webサイトのトップレベルドメインから取得したリソース、つまりWebサイトでホストしているリソースにはdns-prefetchを使用しないでください。MDNでdns-prefetchの使用に関する詳細な情報はこちら。
接続イベントとTLSネゴシエーションイベントを高速化できるのか?
その解決策は、HTTPSを使わないこと。……冗談です。
TLSネゴシエーション時間に関する結論は、2010年にGoogleがGmailをすべてHTTPSに切り替えた後でさえ、TLSは「もう計算コストが高くない」と宣言されました。
2013年に出版された「High Performance Browser Networking」の中で、イリヤ・グリゴリックは『かつては専用のハードウェアが必要だったものが、今ではCPUで直接実行できるようになったのです』と述べています。
イリヤが2013年に出したアドバイスのひとつは、TLSセッション再開をフル活用することです。これは『複数の接続間で同じネゴシエートされた秘密鍵データを再開または共有する』ために使われるメカニズムです。
要するに、あなたのコンピューターとWebサイトがお互いを記憶するための方法で、再接続のたびに暗号化キー(秘密のパスワード)を確認する長いプロセスを経る必要なくなります。これによってブラウジングが速くなり、計算能力も少なくて済むという仕組みです。サーバーでTLSの実装を直接担当しない限り、TLSネゴシエーションを可能な限り高速化することは、おそらく99.999%あなたのために行われます。
しかし、「rel=”dns-prefetch”」を使ってブラウザに必要なリソースのヒントを与えるのと同じように、さらに一歩進んで外部リソースへのリンクに「rel=”preconnect”」を使うことで、TLSネゴシエーションの一部または全部を先取りすることができます。この場合もPerformanceNavigationTimingイベントの後に行われますが、それでも良い情報です。
ブラウザのリクエストとレスポンスのイベント(TTFB)を高速化できますか?
開発者として、Time to First Byte(responseStart)は最終的にページナビゲーションのタイムラインで最もコントロールできるものです。requestStartイベントとresponseStartイベントの間に起こるすべてのことに気を配り、これらのイベントを最適化する際に非情に効率的であることは、ページの速度と結果として得られるユーザーエクスペリエンスに大きな影響を与えることができます。
ここでは、あなたのWebサイトやアプリで調査すべき3つのことを紹介します。
リクエスト・ウォーターフォールを減らす。または無くす方法
「リクエスト・ウォーターフォール」とは、リソース(コード、データ、画像、CSS など)に対するリクエストが、リソースに対する別のリクエストが終了するまで開始されないことを指します。
PerformanceNavigationTimelineで言えば、requestStartイベントがresponseStartイベントを遅らせることがありますが、これはWebページやアプリケーションのアーキテクチャや、ブラウザが最初の1バイトのデータを受け取るまでに同期イベントがいくつ発生するかによって異なります。
ページロードが耐え難いほど遅くなったことに気づいた後、Sentryで状況を調査したところ、各ページロードがエッジサーバーに何度も往復していることがわかりました。これらのジャストインタイム・リクエストを完全に削除し、必要なデータを静的ページビルドに含めることを選択することで、TTFBを根本的に80%削減することができました。おそらく、アプリケーションはrequestStartイベント時に一連のデータベース呼び出しを行うでしょう。
これらのクエリは直列に行う必要があるのでしょうか。それとも並列に行うことができるのでしょうか?最善策としては、1回のクエリでDBから必要なデータをすべて取得できることです。Reactがお好みなら、Reactでフェッチ・ウォーターフォールを特定する方法に関するLazarの投稿をチェックしてください。
DBへのジャスト・イン・タイムの呼び出しは必要ないのでしょうか?
あるいは、私に従ってWebページを静的に構築し、requestStartの後に必要なのは、CDN(コンテンツ・デリバリー・ネットワーク)から静的なHTMLページを素早く配信することだけなのでしょうか?
※これはWebページのインタラクティブ性を強化したり、ページがロードされた後にクライアント側のJavaScriptで新しいデータを取得したりできないという意味ではありません。
キャッシュ、キャッシュ、キャッシュ!
もしあなたのWebサイト(またはそのページの一部)がパーソナライズされた、あるいはダイナミックなコンテンツを提供していないのであれば、キャッシュを活用すべきです。このレベルの設定について考える必要さえない最新のホスティング・ソリューションを使ってWebサイトを配信しているフロントエンド開発者として、私はキャッシュの専門家のふりをするつもりはございません。
しかし、Googleの記事「Optimize Time to First Byte(最初のバイトまでの時間を最適化する方法)」から、この情報を共有していきます。
- 頻繁にコンテンツを更新するサイトでは、キャッシュ時間が短くても、その間に最初の訪問者だけがオリジン・サーバーに戻る遅延を体験します。一方、そのほかの訪問者は、エッジ・サーバーからキャッシュされたリソースを再利用できるため、多忙なサイトでは顕著なパフォーマンス向上が期待できます。
HTMLストリーミングのパワーを活用
HTMLストリーミングとは、HTMLドキュメント全体を一度に提供する代わりに、サーバーがHTMLドキュメントの一部を断片的に時間をかけて送信することです。
ブラウザはこれらのHTMLの断片を受信し、解析やレンダリングを開始することができます。HTMLストリーミングでは、requestStartイベントとresponseStartイベントの間にHTMLドキュメント全体の受信を待つ代わりに、responseStartイベントをより早く発生させることができるため、TTFBを減らすことができます。
Reactエコシステムで作業していて、さらに詳しく知りたい場合は、Lazar氏が『The Forensics of React Server Components』という記事で、HTMLストリーミングについて詳しく説明しています。
知識は力なり
ブラウザがWebページの最初のバイト(Byte)のデータを受信する前に起こることに関するこれらのデータはすべて、かなり強力なものです。しかし、本当の力は、Sentry Trace View でそのデータをコンテキストに置くことにあります。PerformanceNavigationTimingのイベントと問題を視覚化してトレースすることで、タイムラインの遅い部分を粒度レベルで効果的にデバッグし、最適化を可能な限り行うことができるようになります。
もし、あなたのWebページやアプリケーションが可能な限り高速であるなら、この記事があなたに有益な情報を与えることを願っています。もしかしたら、DNSに関する新しい知識を使って、あなたが出席するクールなパーティーで人々を驚かせることができるかもしれません。
ぜひ、以下の記事を読んで、Webサイトのパフォーマンス向上についてさらに学んでください。
- Reactでフェッチウォーターフォールを特定する方法
- バックエンドの問題が原因で遅いページをデバッグする方法
- LCPスコアの低下を引き起こすバックエンドの問題をデバッグする方法
- プロファイリングを活用してボトルネックを特定する方法
また、パフォーマンス・モニタリングを始めるにあたって困ったことがあれば、いつでもDiscordやGitHub、Xでご連絡ください。
IchizokuはSentryと提携し、日本でSentry製品の導入支援、テクニカルサポート、ベストプラクティスの共有を行なっています。Ichizokuが提供するSentryの日本語サイトについてはこちらをご覧ください。またご導入についての相談はこちらのフォームからお気軽にお問い合わせください。