フロントエンドチームの @urahiroshi です。Mercari Advent Calendar 2018 12日目を担当します。
今年の8月から12月にかけて,メルカリ・メルペイのフロントエンドチーム有志で「超速!Webページ速度改善ガイド」の社内輪読会を行いました。
この本の中で,「推測するな,計測せよ」という言葉が紹介されていますが,今回の記事は,輪読会で学んだことのまとめとして,Chrome DevToolsを用いてメルカリWebのパフォーマンス計測と簡単な分析を行ったものです。
なお,現在メルカリWebのアーキテクチャを刷新するための開発が進んでいるため,ここで計測したパフォーマンスの値は大幅に変化していく可能性が高いです。アーキテクチャの刷新後に振り返って見られる楽しみが増えることも、この記事を書いた目的の一つです。
計測方法,環境
計測はGoogle Chrome 71.0.3578.80のDevToolsのPerformanceパネルで行いました。
エンジニアのPCのスペックやネットワーク環境は,スマートフォンの環境などと比較すると性能が高いため,そのまま計測すると多くのユーザーの体感とは異なった結果が出てしまう可能性があります。そこで,今回の計測では,Performanceパネルで Network: Fast 3G
,CPU: 4x slowdown
を選択し,CPUやネットワークを低速化した環境をシミュレーションして計測を行っています。
上の図の Start profiling and reload page
ボタン(赤枠で囲っているボタン)をクリックすることで,現在表示しているページの計測を行うことができます。
なお,今回は紹介しませんが,Performanceパネルでの計測ではなく,Auditsパネルで Run audits
を実行することで,Lighthouseを利用したパフォーマンスの計測やパフォーマンス改善のためのヒントを参照することもできます。audits実行後のページで View Trace
をクリックすることで,Performanceパネルの情報も合わせて参照できるため,First Meaningful Paintなどの指標と比較して計測を行いたい場合はこちらを使っても良いでしょう。
Overviewセクション
計測が終わると計測結果がPerformanceパネルに表示されます。
Performanceパネルの上部には,FPS,CPU,NET,スクリーンショット,Heapの項目からなるOverviewセクションが表示されます。
各項目は以下のような情報を示しています。
FPS
Frame Per Secondの略です。画面(Frame)の変化の速さを示しており,速いほど縦軸の値が大きく,短い時間で画面が更新されていることを示します。Webサイトを閲覧する人にとって,「動作がなめらかに動く」「応答が速い」と感じる指標になります。
一般的なディスプレイのリフレッシュレートは60FPS(約16.7msでフレームを更新する)であり,60FPS以上に速度を上げてもディスプレイの表示速度の方が遅くなってしまうため,60FPSが一つの目標値として挙げられます。
CPU
グラフの高さがCPU負荷の大きさ,色が処理の種類(以下)を示しています。また,斜線が引いてる箇所はメインスレッド以外の処理を示しています。
- 青色 (Loading): HTTPリクエストやHTMLのパース処理
- 黄色 (Scripting): JavaScriptで行われた処理全般
- 紫色 (Rendering): スタイルの評価やレイアウト算出
- 緑色 (Painting): ペイント処理や画像のラスタライズ(ビットマップ化)
- 灰色 (Other): その他
NET
ネットワーク通信状態を示しています。濃い青色のものは優先度が高いリソースへのリクエストを示し,薄い青色のものは優先度が低いリソースへのリクエストを示しています。
リソースの優先度の高さについては,Webページの構成に必須となるHTML,CSSなどがブラウザによって高い優先度として扱われ,画像などは低い優先度として扱われますが,<link>
要素でのrel="prelaod"
指定によるコンテンツの先読み や,HTTP/2を用いたクライアント側での優先度制御(Priority Hintsが策定中です)により,HTML側の定義で優先度をコントロールすることもできるようになってきています。
スクリーンショット
その時間におけるスクリーンショットを示しており,マウスをホバーすると拡大して表示されます。
Start profiling and reload page
を実行する計測の場合,最初の方にリロード前のページのスクリーンショットが表示されるので少し紛らわしいです。
Heap
JavaScriptのヒープ領域のメモリ使用量を示しています。今回はネットワーク,CPUの処理を中心に見ていくため,詳細は割愛します。
Detailセクション
Overviewセクションで詳細を見たい範囲を選択すると,Detailセクション(Network, Frames, Interactions, Main, Raster, GPU, ScriptStreamer thread)にその時間帯の詳細が表示されます。
この記事では主にNetworkとMain(メインスレッド)の処理を参照するので,まずはそれらの表示内容の意味を確認します。
Network
様々な色の棒グラフが表示されていますが,これはリソースの種類ごとに以下のように色分けされています。
- 青色: HTML
- 紫色: CSS
- 緑色: 画像
- 黄色: JavaScript
- 灰色: フォント
また,棒グラフは灰色の直線から始まり,薄い色の四角形,濃い色の四角形,灰色の直線という順で終わっています。
これは,以下のようなネットワーク処理の段階を示しています。
- 左側の灰色の直線: リクエストを送信するまでの時間。サーバーとの接続に時間がかかったり,他に優先度の高い処理を行っているとこの時間が長くなります。
- 薄い色の四角形: リクエストを送信し,最初のデータを受信するまでの時間(TTFB = Time To First Byte と呼ばれます)。サーバーの処理時間や,クライアントとサーバー間のネットワーク経路の状態,物理的な距離によって長くなります。
- 濃い色の四角形: TTFBからすべてのデータを受信するまでの時間。データのサイズが大きいと長くなります。
- 右側の灰色の直線: データ受信後,メインスレッドがデータを処理するまでの時間。メインスレッドが他の処理を行っているとこの時間が長くなります。
また,棒グラフの左上隅に小さい四角形が表示されていることに気がつくでしょうか。これはそのリソースの優先度を示しており,優先度が高いほど四角形の色が濃くなっています。
なお,一番最初にリクエストされているHTML(青色の棒グラフ)のTTFBが確認できませんが,これは計測を開始して間もない時間帯の情報が正常に表示されていないものと思われます。Networkパネル上で対象のリソースを選択し,Timingsタブを参照することで詳細が確認できるため,そちらを確認してみましょう。
TTFBは569.14msであることがわかります。
また,リクエストを送信するまでに, "DNS Lookup" と "Initial connection","SSL" という処理時間が表示されていることもわかります。
クライアントからサーバーにリクエストを送信する場合,以下のようなネットワーク処理が行われるため,それらの処理時間に対応しているものです。
- DNS Lookup: DNSサーバに問い合わせ,ドメイン名からIPアドレスを取得する処理
- Initial connection: 1で取得したIPアドレスのサーバに対し,TCP接続を確立する処理
- SSL: TCP接続確立後,SSL接続を確立する処理(HTTPS接続の場合のみ)
これらの処理時間は,クライアントやサーバのパフォーマンスだけに依存するものではなく,クライアントとサーバ間での通信を何往復か行うため,クライアントとサーバ間の物理的距離に応じても長くなります。サーバが国外にあれば一往復で数10ms以上,国内にあっても10msを超える場合もあり,いくらパフォーマンスを増強してもこの時間を短縮することはできません。
メルカリWebの場合,クライアントはメルカリのサーバと直接通信するのではなく,CDN(Contents Delivery Network)を通じてリソースを配信しています。CDNは物理的に多くの地点にサーバを持っており,クライアントから近い地点のサーバで通信することができるため,これらの時間は比較的短くなっています。
また,HTTPのKeepaliveにより,クライアントとサーバ間で一度TCP接続が確立すれば,一定時間はそのTCP接続を再利用できます。
上の画像では,HTMLへのリクエスト後にCSSにリクエストしている部分のネットワーク処理を示しています。ここでは,DNS Lookup,Initial connection,SSLの処理は実行されず,すぐにリクエストを送信できていることがわかります。
Main
メインスレッドの処理を実行している時間帯が,処理の種類によって色分けされて表示されています。この色分けはOverviewセクションの色分けと同じ意味なので,色分けはそちらの説明を参照してください(Summaryタブにも記載されています)。
今回,測定時に Disable JavaScript samples
のチェックを外しているため,スクリプト処理の部分ではJavaScriptのメソッドの呼び出し階層が表示され,どの処理に時間がかかっているかを分析しやすくなっています。
ただし,このチェックを外した状態だとパフォーマンス計測時の負荷が大きくなってしまうため,より実態に即した値を取得したい場合はチェックを付けた上で計測した方が良いでしょう。
また,下のSummaryタブには,各処理に費やした時間が円グラフで表示されており,選択した時間帯でどれだけ負荷がかかっているか(Idle以外の割合),何の処理に負荷がかかっているかを識別するのに便利です。
HTML,各種リソースのリクエスト
それでは,これからOverviewセクションの時間帯を区切っていきながら,各時間帯の状態を確認してみます。まずはHTMLのダウンロードと,そのHTMLが参照しているリソースのダウンロード処理です。
HTMLのダウンロード後,メインスレッドの処理に一定の時間がかかっています(図の(1))。これは主にHTMLのパースに行われている処理で,HTMLのサイズが圧縮前で517KBあるのに対し,Summaryタブ上で65ms前後の処理時間がかかっています。
その処理と並行して各種リソースのリクエスト処理が行われています。画像ファイルのリクエストに少し待ち時間がありますが(図の(2)),この画像ファイルはURLのドメインが異なるため,DNS Lookup,Initial Connection,SSL接続の時間がかかっているためです。
CSSのダウンロード,レンダリング
CSSのダウンロード後(図の(1)),メインスレッドでCSSのパース処理(約32ms,図の(2)),レンダリング処理(約406ms,図の(3-1),(3-2)),ペイント処理(約28ms,図の(4))が実行され,その後にフレームが更新されていることが確認できます。レンダリング処理はRecalculate Style(約71ms,図の(3-1))とLayout(約334ms,図の(3-2))の2種類に分かれており,Recalculate StyleはCSSのセレクタにマッチする要素にスタイル情報を適用するための時間であり,Layoutはスタイル情報をもとに各要素がどの位置に配置されるかを算出する時間となります。
今回はメインのCSSのダウンロードを起点としていますが,JavaScriptで動的にスタイルの変更を行った場合,レイアウトの再算出が必要となる場合があります。特にループ処理などでレイアウト情報の参照と更新が頻繁に行われていると,レイアウトの再算出が過度に発生するLayout Thrashingと呼ばれる状態になりかねないため,参照と更新のタイミングをまとめるなどして処理を見直す必要があるでしょう。
JavaScriptのダウンロード,実行
JavaScriptファイルがダウンロードされた後(図の(1)),メインスレッドがスクリプティング処理を実行しています(図の(2))。
並行して画像ファイルなどのダウンロードが行われていますが,メインスレッドがスクリプティング処理に集中しているため,ダウンロードしたリソースがメインスレッドに渡されるまでの時間がかかっていることがわかります。
メインスレッドの詳細では,スクリプトの呼び出し階層ごとに処理時間が表示されているため,どこに処理時間がかかっているかを調査するのに使えます。また,メインスレッドの Evaluate Script
の部分をクリックし,SummaryからBottom-Upタブを選択すると,時間がかかっているメソッドの順に並び替えて表示することもできます。
メルカリWebではSPA(Single Page Application)用のライブラリを使用してJavaScriptのダウンロード直後にHTMLのDOM要素の読み書きを行っているため,最初に画面を表示するまでのスクリプト処理の負荷が大きくなっています。
Next.jsなどのSSR(Server Side Rendering)フレームワークを用いると,初回アクセス時のHTMLをサーバー側で計算して返すことができるため,初回アクセス時の負荷は減る可能性があります。サーバー側の処理負荷が増えるためTTFBが増加する可能性はありますが,特に非力なクライアントに対しては効果が見込まれるのではないでしょうか。
各種リソースのダウンロード
JavaScriptのスクリプティング処理の後,フォントファイルや画像ファイルのダウンロードが行われ,それに応じてフレームが更新されていることがわかります(図の(1))。
しかし,メインスレッドではレイアウト処理がたびたび発生しており(図の(2)),その間にレンダリングが行えずフレームの更新が遅くなっていることが確認できます。これはJavaScript側の処理を見直す余地があると言えるかもしれません。
画像のダウンロード
画像ファイルのサイズが大きいため,低速な環境ではダウンロードにかなり時間がかかっている(最大のファイルで699KBに対し8.89秒,図の(1))ことがわかります。
画像ファイルのサイズへの対策として,まず画像の圧縮形式や圧縮率を見直すことが考えられます。
また,ユーザーのスクリーンサイズが小さい場合,不必要に大きいサイズの画像をダウンロードしている可能性があるため,ユーザーのスクリーンサイズに応じて画像のサイズを切り替える方法も効果的です。
<img>
要素に対する srcset
属性や, <picture>
要素,<source>
要素などを用いることで,ディスプレイの解像度や画像の横幅,ブラウザが対応している画像の形式などによって画像を切り替えることができます。
また,HTML Client Hintsでは,HTTPヘッダにクライアントの情報を送り,サーバが適切なリソースを返すための標準化仕様が提案されていますが,現状ではChromeしかサポートしていません。
この他にも,優先的に表示させたい画像であればPreloadによりリソースの先読みさせるという方法も考えられます。
まとめ
以上の計測により,どのようなリソースや処理が表示速度の低下に繋がっているかを可視化できたかと思います。
ただ,個人のPC端末で計測した場合,ネットワーク環境やPC端末の処理負荷はその時々の状態に依存し,また他の人が計測した結果と異なる可能性もあります。そのため,個人の端末での計測した結果を用いて最適化を行うことは,早すぎる最適化につながるリスクがあります。
パフォーマンスの計測を定期的に行い,それを比較するための方法として,Real User MonitoringとSynthetic Monitoringという2種類のモニタリング方式があります。
- Real User Monitoring: クライアント側でパフォーマンスの測定を行うスクリプトを実行し,その結果を送信・集計して「実際のユーザーのパフォーマンス」の統計情報を取得するために使われるモニタリング方式です。実態に合った統計的なデータが取得できる一方,個々のデータはユーザーの環境によってばらつきがあり,クライアント側でスクリプトを実行するため,そのスクリプト自身の処理負荷が増えてしまうなどのデメリットもあります。
- Synthetic Monitoring: クラウドサービスなどの環境から定期的にリクエストを送信し,そのパフォーマンスを測定するモニタリング方式です。同一の環境からリクエストを行うため,取得できるデータ間の差異は少なくなるためパフォーマンスの基準値にしやすいですが,実行環境とユーザーの環境に差があると実態に即していない結果を表示してしまう可能性があります。
これらの計測によりパフォーマンスの低下を察知し,Chrome DevToolsにより詳細を分析した上でパフォーマンスのボトルネックを突き止めて対処することが理想的なフローと言えるでしょう。
今回記載している内容の多くは「超速!Webページ速度改善ガイド」に詳細が記載されているため,興味を持った方はぜひ書籍を手に取ってみてはいかがでしょうか。
それでは,Mercari Advent Calendar 2018 明日13日目の担当は @pospome となります。引き続きお楽しみください。