SPA (Single Page Application)のパフォーマンス計測に関する内容です。
CSR (Client Side Rendering)時の画面遷移の速度を計測し、他のサイトでの計測結果と比較したいと思いました。
やりたいこと
- CSR (Client Side Rendering)時の画面遷移の速度を計測したい
- 稼働中のWebサイトで、コードを一切変更せずパフォーマンス計測したい
- 複数のサイトを計測して比較したい
背景
jQueryを使ったMPA -> Next.jsでのSPAへのWebシステムリプレース案件を担当しました。
機能を変更せずそっくりそのままアーキテクチャだけ入れ替えるプロジェクトであり、プロジェクトを完遂した今、同じ機能を持つアーキテクチャ違いのWebシステムが2つある珍しい状況にあります。この際なのでMPA、SPAの置き換え前後のパフォーマンス比較を行いたいと思いました。
MPA(Multiple Page Application)からSPAへの置き換えなので、より顕著に差が出ると予想されるページ間の画面遷移の時間にフォーカスして性能を計測してみたいと思いました。
やったこと
既存ツール探し
既存のツールで目的を果たせるものがないかを探しました。
Lighthouse
Webフロントのパフォーマンス計測ツールとして最も有名なのはLighthouseですが、今回の目的には適していません。
特定のURLの初期ロードの速度を計測する上では非常に優れたツールですが、画面間の遷移速度を計測することができません。
GoogleChromeのパフォーマンスタブ
計測できそうに見えましたが、単体では目的を果たせませんでした。
描画開始時刻は画面遷移のためのクリック操作の時刻が記録されるので取得できますが、描画終了時刻はこのツール単体では取得できません。
描画完了時にはイベント等何も発火しないため、タイムライン上にその時刻を記録することができないためです。
自作ツールの作成
要件に沿ったパフォーマンス測定を行うためにツールを自作しました。上記のようにブラウザのパフォーマンスタブ単体では描画完了を判断できなかったので、そこを補うようなツールになっています。
一応OSSとして公開していますが、幅広く使ってもらえるところまで作り込んでいないので使いづらい部分が目立つと思います。
https://github.com/Kontam/rendering-timer
特別珍しいAPIなどは使用していないので、この記事ではプログラムの内容については述べません。
上記のリポジトリを参照してください。
実行結果例
下記2つのサイトの画面遷移を比較してみました。
それぞれ5回ずつ測定し、平均値を出力しています。
測定用のシナリオファイルをそれぞれのサイト用に用意することで、1コマンドで両サイトの計測を行って良い感じに標準出力することができます。
結果としては、東京都ページの遷移の方が平均して100ms程度早いということになりました。
(あくまでこのツールでの測定結果です。京都府のページは遅いなどと安易に解釈しないようにお願い致します)
ざっくりした計測ロジック
以下のような手順でパフォーマンスを計測します。
- puppeteerでブラウザを動かし、計測対象の画面遷移操作を実行してGoogleChromeのパフォーマンスプロファイルを取得する
- パフォーマンスプロファイルに含まれるスクリーンショットを利用して描画完了時刻を算出する
- パフォーマンスプロファイルに含まれる「画面遷移のトリガとなったクリックイベントの時刻」と「描画完了時刻」の差分を算出する
測定の詳細な流れ
パフォーマンスプロファイルの取得
上のざっくりした計測ロジックの中で登場するパフォーマンスプロファイルとは GoogleChromeのパフォーマンスタブで取得できるタイムラインのことです。
puppeteerを利用することで、測定を行いたい画面操作をブラウザに実行させパフォーマンスタイムラインをjson形式で取得することができます。
この時に取得できるjsonのプロファイルはGoogleChromeのパフォーマンスタブからexportできるjsonと全く同様のものです。puppeteerで出力したjsonをブラウザに読み込ませて結果を閲覧することもできます。
使用したAPIがこちらです。
puppeteer.tracing | Puppeteer
描画完了時刻の取得
ここが一番の難題です。画面遷移の時間を測る上で、「画面遷移が完了した」という判断は何をもってすれば良いでしょうか。
パフォーマンスプロファイルのタイムラインを見ても、「画面遷移が完了した」ことを表す情報はありません。何の定義もなくWebサイトの最終状態を機械的に判断することはできません。
そのため、エンジニアが定義した描画完了状態でJavaScriptを動作させるようなパフォーマンス計測手法がよく見られます。
今回は他者が作成したWebサイトと比較することを目標としているため、サイト自体に細工を加えないアプローチを取ります。
判定には、パフォーマンスプロファイルに記録されているスクリーンショットを利用します。
このスクリーンショットはpuppeteer経由で取得できるjsonにも含まれており、BASE64エンコードになっています。
**タイムラインの最後のスクリーンショットはUIが最終状態になっていると考えられます。**しかし、UIに変化がない時にも取得されていることがあるので描画完了直後の状態ではありません。
描画完了直後の時刻を取るために、最後のスクリーンショットとそれより前のスクリーンショットを後ろから一つずつ比較していき、最初に差分が出たところを描画完了時刻とします。
私の自作アプリケーションのページのトップからGet Startedへのページ遷移を例にして紹介します。
まず、プロファイルの中の最初のスクリーンショットについて見てみます。最後のスクリーンショットを画像比較すると、まだ遷移前なので、大量に差分が出ています。
次に、プロファイル中の最後のスクリーンショット同士の比較結果が以下です。同じ画像の比較ですので、当然差分はありません。
この画像のように最後のスクリーンショットと差分がない状態 = 描画完了していると判断できます。つまり、プロファイル中で最初に差分ゼロとなる画像がどこで現れるかを探せば良いということになります。
このために最後のスクリーンショットと後ろから二番目、三番目...と比較していくと、いずれ差分のあるスクリーンショットが発見されます。
この差分が初めて検出される1つ前のスクリーンショットのタイムスタンプが描画完了時点ということになります。
画面遷移時間の算出と結果出力
画面遷移にかかった時間は 描画完了時刻のタイムスタンプ - 画面遷移開始時刻のタイムスタンプ で求められます。
画面遷移開始時刻 = 次の画面に遷移するためのリンクをクリックした時なので、これはパフォーマンスプロファイルに記録されています。
あとは算出した時間を画面表示するだけです。お察しの通り、正確性を重視した方法ではないので複数回測定して平均値を算出する機能をつけています。
他に検討したアプローチ
描画完了時刻を取得するために検討したアプローチは他にもいくつかあります。
MutationObserverによる描画完了の検知
一つ目はMutationObserverを使用する方法で、こちらの方が精度が高いと思います。このAPIでDOMの変更を検知することができるので、「特定のDOMがツリーに現れたら描画完了とする」という判定を行うことができます。
puppeteer経由でコードを挿入して実行することもできるため、計測対象のWebサイトのコードを変更せずに計測を行うことができる点でかなり今回の目的には沿っています。
それでも今回採用しなかったのは、「特定のDOMがツリーに現れたら描画完了とする」という判定方法が少々エンジニア目線すぎると感じたためです。
この判定方法はエンジニア外の方にはわかりづらく、計測方法の説明がやや複雑になるためマーケ担当等への情報共有の目的には使いづらいと考えました。採用したスクリーンショットベースの判定方法であれば、実際に判定に使用したスクリーンショットを見せて「リンクをクリックしてから画面がこの状態になるまでの時間を計測しました」という説明が可能なため、非常に直感的だと思いました。
計測対象のWebサイトにログを仕込んで計測する
こちらが一番最初に思いついたシンプルな方法です。Webサイトにパフォーマンス計測用のログを仕込んで計測します。
描画完了の判定もReact.useEffectでコンポーネントのマウント完了時にログが出るようにすればある程度正確なタイミングが取れると思います。
ただ、冒頭から述べている通り今回の要件として「計測対象のコードに手を加えない」というものがあるので、今回この方法は取りませんでした。
今回のパフォーマンス測定で意識したこと
一言で言えば、目的を果たすために必要なことを考えることです。以前から意識していたことですが、今回はそれを高い水準で達成できたと思います。
ただのパフォーマンス測定であれば、前述の通りLighthouseのような優れたツールが既に存在します。それで目的を果たせるならそれを利用するのが最良と言えます。
今回ツール作成に至ったのは目的を達成するために必要だと判断した結果です。また、そのツールの仕様を考えるにあたっても目的へのフォーカスを強く意識しました。
OSS化する前提だったとはいえ、広く使ってもらえるほどの知名度を個人制作物で稼ぐのは難しいですし他にやることもたくさんありましたので、かけられるリソースは限られています。目的にフォーカスしなければ作り終えることすらできなかったと思います。
目的を意識し、達成した要件と切り捨てた要件はざっくりと以下です。
達成した要件
- 画面遷移速度を計測できる
- 複数サイトを1コマンドで計測できる
- 複数回計測して平均値を算出する
- 計測に使用したスクリーンショットをエビデンスとして確認できる
切り捨てた要件
- アニメーション付きのサイトで計測
- 早い計測速度
- 蓄積した計測結果のビジュアライズ
今回制作したツールは狙い通り気軽に使える物に仕上がったので個人的には気に入っています。
自分のサイトのパフォーマンス監視のために、切り捨てた要件を付け加えて行っても面白いかもなぁとは思っています。
参考までにSPAとMPAを比較した結果
冒頭で述べた、全く同じ機能を持つSPAとMPAの比較結果です。
DBのデータ量等まで厳密に揃えた比較結果ではありませんし、詳しい前提はここでは説明しませんのでこんな結果だったよという参考程度に。
- SPA: Next.js + Spring Boot(Java)
- MPA: jQuery + Flask(Python)
SPAの方はSWRを採用しており、せっかくなのでデータキャッシュ適応時のページ遷移速度も測定しています。
比較結果
画面A → 画面Bの遷移時間の比較です。SPAとMPAはアーキテクチャだけの違いで、全く同じ機能の画面で比較しています。
SWRキャッシュの測定は、画面A → 画面B → 画面A → 画面B と操作して、2回目の遷移の方だけ測定しています。
SPAの方が平均して100[ms]ほど早く、SWRキャッシュが乗っているとさらにもう100[ms]早いという結果になりました。
この結果を使い、最大で6倍程度高速化できたという報告でモダンFEリプレースの威力をアピールすることができました。