Web パフォーマンス分析の概観

この記事では、Web サイト・Web アプリケーションのパフォーマンス分析のプロセスの概略を説明します。

筆者は JavaScript とその周辺技術を専門としているため、この記事ではフロントエンドからの分析が中心となり、バックエンドやインフラの分析は基本的に扱いません。
しかし、まずはフロントエンドから分析を行い、Web ページ全体のパフォーマンスを把握することで、バックエンドやインフラにボトルネックがあるのか否かを判断するための材料とすることが出来ます。

# 1. Chrome Developer Tools で分析する

まずは Chrome Developer Tools を用いて分析します。

## ネットワークパネル: ウォーターフォールチャート

Chrome DevTools の Network パネルにある棒グラフのような図をウォーターフォールチャートと呼びます。開発手法のウォーターフォールとは特に関係ありません。

![Chrome DevTools のウォーターフォールチャート](https://qiita-image-store.s3.amazonaws.com/0/41297/3937fc10-88d2-5002-ef59-572ae2803e0c.png)

ウォーターフォールチャートでは、HTML やアセット類それぞれについて、HTTP リクエストを発してからダウンロードが完了するまでの速度を見ることができます。

画像や JavaScript など静的コンテンツのダウンロード完了までに時間がかかっている場合、大雑把には、

1. サーバーが重い
2. ネットワークの速度が遅い

のいずれかが原因として考えられます。[^1]

しかし、ネットワークの速度は、例えば携帯回線などは電波状況によって遅くなる場合などもありますが、これは開発者側で手を出せる部分ではないため、気にしても意味がありません。(逆に、基本的には計測する際には光回線を用い、安定したネットワーク状況の下で行うことが望ましいです。)

従って、基本的にはサーバーを高速化させる方向で検討することになります。サーバーの設定を見直す、サーバースペックを上げる、高速な CDN を導入する (導入済みの場合はより高速な CDN を検討する) などの対応が考えられます。

[^1]: 低速な回線 (格安 SIM、海外のインターネット回線が低速な地域など) においては、ファイルサイズが重すぎることが速度低下の原因となっている場合もあります。しかし、日本国内における最近の光回線や 4G 回線であれば、問題にはなるケースは少ないと思われます。日本国内の光回線で計測して速度が遅い場合、基本的にはファイルサイズが原因ではないと考えるべきです。

MPA (非 SPA) における HTML や、Web API として返される JSON など、動的コンテンツが遅い場合は、バックエンドアプリケーションの処理が遅いことも可能性として考えられます。
従って、考えられる原因は、主としては下記の3つになります。

1. サーバーが重い
2. ネットワークの速度が遅い
3. バックエンドアプリケーションの処理が遅い

実際はフロントエンドの実行処理 (例えば JS の処理など) が絡んで遅延する場合などもあり、もう少し複雑ですが、はじめの一歩としては、ウォーターフォールチャートで検出できる遅れは、サーバーかバックエンドに起因するものと考えて頂いて良いと思います。

## Performance パネル: パフォーマンスチャート

Chrome DevTools の Performance パネル中段の Main とある部分では、JavaScript や CSS など、フロントエンド側の**処理** (ダウンロードではなく、スクリプト等の実行) について分析することができます。
どの JavaScript ファイル、どの JavaScript の関数で、どの程度処理に時間がかかっているか、などがわかります。

![Chrome DevTools のパフォーマンスパネル](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41297/b9ee7c2e-14c6-9a22-88b1-51b66bbcfbdd.png)

青いラインが HTML のパース処理、主に黄色いラインが JavaScript の処理です。
Network パネル同様、横に長いほど処理に時間がかかっています。
例えば JavaScript の場合、ラインをクリックすると、どのファイルのどの行 (どの関数) の処理でそれだけの時間がかかっているかを見ることができます。

# 2. 分析の指標について

Web パフォーマンス分析では、ユーザーがリンクをクリックするなどしてから、あるタイミングを迎えるまでにかかった時間を取得します。
指標としては色々あるのですが、主に以下のような指標があります。

- First Paint
  - ブラウザーがレンダリングを開始するまでのタイミング ([W3C Spec](https://w3c.github.io/paint-timing/#first-paint))
- First Contentful Paint
  - ページ上で何らかの要素が表示されるまでのタイミング ([W3C Spec](https://w3c.github.io/paint-timing/#first-contentful-paint))
- Largest Contentful Paint
  - ページ中で最も大きい要素が表示されるまでのタイミング ([Google の定義](https://web.dev/lcp/))
- DOM Content Loaded
  - HTML パースが完了し、その後 `DOMContentLoaded` イベントが実行され終わるまでのタイミング[^2]
  - 個人的にはあまり重視していませんが、Chrome Developer Tools の Performance パネルで表示されるので、参考までに取り上げています。
- load (下の図中では Fully Loaded)
  - HTML パース、画像の読み込み、JavaScript の実行が一通り完了し、`load` イベントが実行され終わるまでのタイミング[^3]

[^2]: 厳密に HTML パースが完了するまでのタイミングを示すのは DOM Complete です。DOM Complete の直後に `DOMContentLoaded` イベントが発生します。[Navigation Timing API](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming) では、`DOMContentLoaded` イベント開始のタイミングを示す `domContentLoadedEventStart` と、完了のタイミングを示す `domContentLoadedEventEnd` に分かれています。Chrome Developer Tools の Performance パネルで青いラインで示される DOM Content Loaded は、`domContentLoadedEventEnd` を意味しているようですので、この記事ではこれに従いました。

[^3]: [Navigation Timing API](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming) では、`load` イベント開始のタイミングを示す `loadEventStart` と、完了のタイミングを示す `loadEventStartEnd` に分かれています。Chrome Developer Tools の Performance パネルで赤いラインで示される Onload Event は、`loadEventStartEnd` を意味しているようですので、この記事ではこれに従いました。

古川洋介さんのスライドがわかりやすいので、引用します。

![First Paint などの指標](https://qiita-image-store.s3.amazonaws.com/0/41297/daff9138-5520-efde-c52d-c5a84b9cc98a.jpeg)
出典: [You need to know SSR - Yosuke Furukawa](https://speakerdeck.com/yosuke_furukawa/you-need-to-know-ssr?slide=16)

First Meaningful Paint は [Lighthouse でも deprecated になる](https://web.dev/first-meaningful-paint/)など、古い指標として捉えた方が良いようですので、無視して下さい。
Visually Ready (Above the fold の表示が完了するまでのタイミング) や Time To Interactive (JavaScript が実行できるようになるまでのタイミング) は、あまり重視されていないようですが、必要に応じて覚えておいても良いかもしれません。

## Speed Index

Speed Index も Web パフォーマンス分析で重視される指標の1つですが、上記の指標とは少し毛色が異なります。
どれだけの速度で、どれだけの面積が描画されているか、という考え方で計算される指標です。

参考: [Speed Index - WebPageTest docs](https://github.com/WPO-Foundation/webpagetest-docs/blob/master/user/Metrics/SpeedIndex.md)

# 3. モニタリングする - Synthetic Monitoring

Chrome Developer Tools の Network パネルでも `load` など各種速度指標を確認することはできますが、1回、または数回計測しただけでは、たまたまその時に速かった、或いは遅かった、という可能性を排除できません。
例えば娯楽系のサービスでは、一般に夜にアクセスが集中し重くなりますから、日中の業務時間中には、夜よりも高速な結果が得られますので、あまり参考になりません。

Synthetic Monitoring (合成監視) と呼ばれるツールで、定期的にそれぞれの速度指標を計測して記録し、グラフに落とし込むことが可能です。

![表示速度のグラフ (Catchpoint)](https://qiita-image-store.s3.amazonaws.com/0/41297/86e5b782-5c1b-03e2-c7fd-cb7ee0a09d70.png)

Synthetic Monitoring ツールには、下記のようなものがあります。

- [Catchpoint](http://www.catchpoint.com)
- [SpeedCurve](https://speedcurve.com)
- [Checkly](https://www.checklyhq.com)
- [Calibre](https://calibreapp.com)
- [New Relic Synthetics](https://newrelic.com/products/synthetics)
- [Lighthouse CI](https://github.com/GoogleChrome/lighthouse-ci)
- [Grafana](https://grafana.com/grafana/plugins/grafana-synthetic-monitoring-app)

その他、[Front-End-Performance-Checklist という GitHub リポジトリーの Performance tools セクション](https://github.com/thedaviddias/Front-End-Performance-Checklist#performance-tools)に、他の Synthetic Monitoring ツールの記載があります。(Synthetic Monitoring ツールでないものも一部含まれています。)

## 計測拠点のネットワークについて

Synthetic Monitoring ツールは、あるコンピューターから Web ページにアクセスし、その際の各速度指標を記録します。
そのため、速度はアクセス元のコンピューターの接続しているネットワークの速度に影響されます。

実際にエンドユーザーが用いる回線 (NTT、KDDI、Nuro など) に繋がったコンピューターから計測するのが一番良いですが、多くのサービスは AWS の EC2 インスタンスなど、クラウドから計測を行っているようです。
クラウドからの計測の場合、一般家庭のネットワークとは異なる回線であるため、例えば以下のようなケースで、正確な計測ができないことは認識しておく必要があります。

- CDN を選ぶにあたり、どれが一番高速であるか比較することができない
  - EC2 から計測した場合、一般回線で計測した場合に比べて、CloudFront にとって有利な結果が出るかもしれません。
- クラウド事業者のネットワークの遅れを検出できない
  - 特定の回線においてのみネットワークが遅延する場合、それを検出することができません。開発者側が直接手を出すことはできない部分ですが、クラウド事業者に言えば直してもらえる可能性もあります。

エンドユーザーが用いる回線からの計測をデフォルトでサポートしている Synthetic Monitoring ツールは、エンタープライズ向けでかなり高額になる印象です。

個人的には、通常のアプリケーション計測では、EC2 インスタンスからの計測で妥協することも選択肢としてあり得ると思います。

いくつかの Synthetic Monitoring ツールでは、監視用のエージェントをセルフホストすることができます。一般の ISP から計測する必要があるケースでは、計測マシンを実機で用意し、家庭のインターネット回線に繋いで計測するという選択肢もあります。
Lighthouse CI など、元からセルフホスト型の Synthetic Monitoring ツールを用いる場合も同様です。

# 4. モニタリングする - RUM (Real User Monitoring)

ページに JavaScript を埋め込み、エンドユーザーがページを閲覧した際の実際の表示速度を取得します。
Synthetic Monitoring は計測専用のマシンからアクセスした結果をデータとして取得しますが、一方で RUM は、エンドユーザーが実際にページを訪れた際の速度を取得することができます。

RUM のツールには、下記のようなものがあります。

- [Catchpoint](http://www.catchpoint.com)
- [SpeedCurve](https://speedcurve.com/)
- [Sentry](https://sentry.io/for/performance/)
- [New Relic Browser](https://newrelic.com/products/browser-monitoring)
- [Cloudflare Web Analytics](https://www.cloudflare.com/web-analytics/)

オープンソースでは、HubSpot が作っている [Bucky](http://github.hubspot.com/bucky/) というツールがあったのですが、開発終了してしまいました。

ただ、セルフホスト型でやるなら、後述の Navigation Timing API 等で値を取得して、データベースに Web API 経由で保存するだけでも、ある程度同じことができるかもしれません。

## Navigation Timing API

[Navigation Time API](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming) を用いることで、JavaScript で DNS の名前解決時間や、DOM Content Loaded、load などまでに掛かった時間を取得することが可能です。[^4]

[^4]: その他の取得可能な指標に関しては [PerformanceNavigationTiming](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming) のリファレンスを参照して下さい。

同様に、[Paint Timing API](https://developer.mozilla.org/en-US/docs/Web/API/PerformancePaintTiming) を用いることで、First Paint と First Contentful Paint を取得することが可能です。(但し、Chrome などでのみ取得可能なようです。Firefox など、まだ未実装のブラウザーがあります。)

```javascript
if (window.performance) {
  //
  // Navigation Timing API
  //
  const navigationTimingEntry = performance.getEntriesByType("navigation")[0];

  console.log(`
    DNS Lookup: ${navigationTimingEntry.domainLookupEnd - navigationTimingEntry.domainLookupStart}ms
    DOM Content Loaded (start): ${navigationTimingEntry.domContentLoadedEventStart}ms
    DOM Content Loaded (end): ${navigationTimingEntry.domContentLoadedEventEnd}ms
    load (start): ${navigationTimingEntry.loadEventStart}ms
    load (end): ${navigationTimingEntry.loadEventEnd}ms
  `);

  //
  // Paint Timing API
  //
  let firstPaint;
  let firstContentfulPaint;

  for (const entry of window.performance.getEntriesByType("paint")) {
    if (entry.name === "first-paint") {
      firstPaint = entry.startTime;
    } else if (entry.name === "first-contentful-paint") {
      firstContentfulPaint = entry.startTime;
    }
  }

  console.log(`
    First Paint: ${firstPaint}ms
    First Contentful Paint: ${firstContentfulPaint}ms
  `);
} else {
  console.log("このブラウザーでは速度情報を取得できません。");
}
```

# Synthetic Monitoring と RUM のそれぞれの意義

既述の通り、Synthetic Monitoring は計測専用のマシンからアクセスし、その際にかかった時間をデータとして取得します。
一方で RUM は、エンドユーザーが実際にページを訪れた際にかかった時間を取得します。

例えば

- [`<script>` タグに defer や async を追加](https://qiita.com/phanect/items/82c85ea4b8f9c373d684)してみて、本当にパフォーマンス向上の効果があったかどうかを確認する
- 何らかの原因でパフォーマンスが突然下がり、ページの表示速度が遅くなったことを検出する (遅くなった直前にデプロイした変更が恐らく原因だとわかる)

など、パフォーマンスの上下を見るには、Synthetic Monitoring の方が適切です。

RUM はエンドユーザーが実際に体験している速度ですから、環境も様々です。
例えば、利用しているネットワークは 1Gbps の光回線から容量を使い切った後の速度制限されている携帯回線までありえますし、WiFi や携帯回線の場合、電波状況も異なります。端末のスペックも様々なので、JavaScript の実行速度などに大きく影響が出ます。
環境条件が多種多様なので、RUM で急に速くなった、遅くなったからと言って、それが自社で行った変更によるものであるとは限らないのです。

Synthetic Monitoring では、ネットワークはだいたい光回線での計測がサポートされているので比較的速度の変化が少なく安定しています。
端末も、基本的には同一のマシンから計測しているでしょうから、スペックが様々なマシンからの計測結果が混ざることもありません。[^5]

[^5]: 複数の地域から計測し、それらの結果を比較する場合、当然地域ごとに別のマシンからアクセスしているはずですので、それらのマシンのスペックが同一であるかどうか、注意して下さい。スペックが異なる場合でも、それぞれ独立した計測として見ることは問題ありませんが、例えば東京より中国の方がアクセス速度が遅い場合でも、サーバーを東京に置いてあるから中国では遅いとは結論付けられず、単に中国の計測マシンのスペックが悪いだけという可能性を否定できません。

ですので、Synthetic Monitoring の方が、エンドユーザーの環境の影響を受けづらく、自分たちが手の届く範囲に限定してデータを見ることが可能です。

一方で、Synthetic Monitoring の環境は、実際のユーザーのマシンスペックやネットワーク環境などより良すぎるかもしれませんし、悪すぎるかもしれません。
例えば、古くスペックの低い PC を使うようなユーザー層が多いサイトであれば、Synthetic Monitoring で高速な結果が出ていても、ユーザーの多くは遅いと感じているかもしれません。

実験室的環境である Synthetic Monitoring だけでは、実際のユーザーの体験を把握することができないため、RUM が必要になってきます。