Webブラウザのレンダリングパフォーマンスを計測する: requestAnimationFrameの活用
こんにちは、ログラスでエンジニアをしております浅井(@mixplace)です。
最近は機能開発チームから離れて、フロントエンド領域の課題を横断的に推進・解決していく「フロントエンドチーム」を組成し、プロダクト開発活動の中で起きるフロントエンドにまつわる課題を、優先度と中長期的な視点から解決すべく取り組んでおります。
その中のひとつに、大量のデータを扱う画面での、Webブラウザのパフォーマンス面の計測・改善があります。
今回は「レンダリングパフォーマンスを計測して、ユーザビリティの良くない箇所を早期発見する取り組み」についてご紹介したいと思います。
なぜレンダリングパフォーマンスを計測することにしたのか
Webフロントエンドの領域において、パフォーマンス改善はとても重要です。
ここ数年では、Core Web Vitals と呼ばれる、ユーザーエクスペリエンスを良くする指標をもとに、Webページのパフォーマンスを改善する取り組みも重要性が高まっています。
Loglass プロダクトにおいては、さまざまな切り口で予実分析が行える特性上、大量のアイテムやテーブルセルを表示する画面がいくつも存在します。
経営分析の要となる部分であり、バックエンド・フロントエンド問わず、いかにして高速に処理・表示できるかが重要なです。
フロントエンド部分では、大量のテーブルセルが描画されうる画面において、仮想スクロール機構を導入することによってWebブラウザパフォーマンス問題に解決の道筋がつきました。(巨大なテーブルコンポーネントを仮想スクロール化してブラウザのメモリ使用量を1/10にした話 の記事もぜひご参照ください)
しかし、ユーザーの使い方の多様化、さらには最近の新機能・既存機能の改善が進んだことで、意図せず大量のデータを表示してしまうケースが他の画面でも見受けられるようになってきました。
事象調査を進めていくと「APIレスポンスには問題ないが、フロントエンド側にのみボトルネックがある」というケースがあることがわかり、このボトルネックポイントを検知できていなかったのでした。
ユーザーやCS(カスタマーサクセス)の皆さんから「表示が遅いです、Webブラウザが重いです」と言われてしまっては、良い品質のサービスを提供しているとは言えません。
そのため今回は、レンダリングに要した時間を記録する仕組みを作って、継続的にモニタリングできるようにしようと考えました。
今後も多くの機能拡充が見込まれるため、予想以上にレンダリングに時間を要しているという“異常値”が見受けられたら、先手を打って対応ができるようになるためです。
加えてWebブラウザのパフォーマンスは、ユーザーが利用している環境(パソコンのスペックやネットワーク環境)にも大きく依存します。ユーザーが利用しているデバイスのパフォーマンスの傾向を知りたい観点もあり、取り組むこととしました。
“レンダリングパフォーマンス”を定義し、計測したい範囲を決める
今回は数千〜数万といったアイテムを表示する場面があり、テーブルコンポーネントやリストコンポーネントの表示に時間がかかってしまうケースがありました。
計測にあたっては、「画面内の一部コンテンツの表示に要した時間を計測する」という範囲にスコープを絞ることにし、Webブラウザのレンダリングフェーズにおける「Rendering + Painting」に要する時間を計測することとしました。
フロントエンドで処理に要した時間を正確に記録するならば「JavaScript の Scripting フェーズで要した時間」も含めて計測できるのが筋ではありますが、Scripting に時間を要する場合は「Rendering + Painting」にも時間を要するであろうという仮説のもと、各テーブルコンポーネント群で汎用的に計測したかったことから、まずはスコープを絞って取り組むことにしました。
どのようにしてページの一部分のレンダリング完了を取得するのか
一般的な、Core Web Vitals で求められているWebブラウザパフォーマンスの指標を計測するのであれば、PerformanceObserver という JavaScript API を用いることで、各フェーズの記録を取得することができます。
しかし、ページ内特定の一部分、具体的にはコンテンツの一部を非同期で描画・再描画する際のレンダリングパフォーマンスについては、具体的数値を取得できる仕組みは確立されておらず、また正確に計測することは難しいとされています。画面上に表示が変わったことをコールバックで返してくれるような仕組みも見つけられませんでした。
「Rendering + Painting」を計測する手法について調べていく中で、 Accurately measuring layout on the web という記事に出会いました。
上記記事でレンダリングパフォーマンスを計測するいくつかの手法が紹介されていますが、今回は読みやすさの観点で requestAnimationFrame のコールバックに requestAnimationFrame を重ねるアプローチで計測する手法で、実際に試してみることにしました。
以下のような、requestAnimationFrameのコールバックをネストさせるコード入れることで、レンダリングが完了するまでの時間を測定しています。
const startTime = performance.now();
requestAnimationFrame(() => {
requestAnimationFrame(() => {
// Rendering + Painting に要した時間を計測。ミリ秒で結果が得られる
const duration = performance.now() - startTime;
});
});
通常、Webブラウザが操作を受け付けられなくなるほどレンダリング処理が重くなるケースは無いかと思いますが、処理が重くなると表示が追いつかず requestAnimationFrame の処理も遅れてしまうことを逆手にとったアプローチと理解しました。
深堀り分析したい指標をDOM変化から取得する
計測ができるようになると、当然、なにが原因で遅くなったのかも知りたくなるはずです。
今回の場合ですと、どれくらいの量のDOM要素を描画しようとしたのか深堀りしたくなるため、MutationObserver を使ってDOM の変更を検知(observe)して取得することとしました。
テーブルセルやリストアイテムなど、大量データ描画が起きえそうなコンポーネントを計測対象とすべく、計測するためのコンポーネント RenderMeasurePerformance
を作成し、レンダリングの変化を検知できるようにしました。
コード例を紹介します。
const MeasureRenderPerformance = ({ children }) => {
useEffectOnce(() => {
const observer = new MutationObserver((mutations) => {
const startTime = performance.now();
requestAnimationFrame(() => {
requestAnimationFrame(() => {
// Rendering + Painting に要した時間を計測。ミリ秒で結果が得られる
const duration = performance.now() - startTime;
// mutations変数内に、変化が起きたDOMデータが入っているので、対象のDOMを抽出したり、数を調べたりすることができる
const { addedNodes, removedNodes } = mutations[0];
// ログ基盤に計測値を送信する(一例)
sendMeasureEvent(duration, { added: addedNodes.length, removed: removedNodes.length });
});
});
});
return () => {
observer.disconnect();
};
});
return (
<div ref={ref} data-measure-name={name}>{children}</div>
);
};
<RenderMeasurePerformance>
<HogeTable>
...
</HogeTable>
</RenderMeasurePerformance>
MutationObserver の引数 mutations
には、追加・削除された NodeList が格納されており、DOM にアクセスが可能です。
測定できた「レンダリング時間」「変更が発生したDOM要素数」の記録は、ログ基盤として利用している Datadog にイベントとして送出することで観測、可視化できるようにしました。
計測してみてわかったこと
上記計測の仕組みを入れて1週間ほど運用してみましたが、結果としてほぼすべてのユーザーにおいて0.5秒以内でレンダリングされていることがわかりました。
Rendering + Painting で多く時間がかかってしまうユースケースは皆無ではあるため、当然の数値ではありますが、今回は「異常にレンダリング時間を要したユーザーがいないかどうか」が課題として重要です。
1週間ほど計測したところ、ある1ユーザーにおいて、特定画面で想定を超えるセル数の描画が発生していることがわかりました。快適に動作しているとは言い難いため、今後対応予定だったパフォーマンスの改善を優先度を繰り上げて取り組むこととしました。
おわりに
フロントエンドのパフォーマンス対策として、一般的には表示できるアイテム数に上限を設けたり、ページネーションを用いることで物理的に大量のアイテムと向き合う必要をなくすことができます。しかし、ユーザビリティの観点から上限を設けずスムーズなユーザー体験を提供したい場面もあるかと思います。
このようなバランスが求められる場面においては、1人でも多くのユーザーに快適に使っていただけるよう取り組めるためのファクトとしては使えるという印象を持ちました。
今回の requestAnimationFrame と MutationObserver を用いてレンダリング時間と変更量を計測する手法で、異常にレンダリングに時間を要した記録がとれて、パフォーマンス課題の早期発見ができるようになりました。
Datadog や New Relic など、ブラウザモニタリングができる APM(Application Performance Management)ツールでもブラウザパフォーマンスの監視はできますが、コンテンツの一部分に特化した詳細度の高い分析を行うには、APMツールを補完する形で自前で作ってみる意義もあると考えています。
今後もユーザーの最適な体験とパフォーマンスのベストなバランスを探っていきながらプロダクトの改善、ユーザビリティの向上に寄与できる取り組みを行っていけたらと考えております。
Discussion