[優れたUXを目指して]アプリの性能について:第2回 | デザインってオモシロイ -MdN Design Interactive-

[優れたUXを目指して]アプリの性能について:第2回

2024.4.24 WED

【サイトリニューアル!】新サイトはこちらMdNについて
[優れたUXを目指して]アプリの性能について:第2回
前回に引き続き、アプリの性能について考えておくべき話を解説していきます。前回は環境と実行回数について触れましたが、今回は描画とメモリについてお話ししたいと思います。

 1. 環境を知る
 2. 実行回数を知る
 3. 描画を知る
 4. メモリを知る
 5. 処理タイミングを知る 
 6. 通信を知る
 7. ビルドを知る

TEXT: 2020年6月12日 上田恵
▷ 描画を知る
描画はフロントエンドのアプリ特有の話だと思います。性能に影響を与える大事な部分ですので、ある程度は知っておいた方が良いでしょう。

まずは描画される回数です。これは前回の実行回数のお話と同じになりますが、描画自体がとてもコストのかかる処理であるため、何回描画されているのか知っておくべきです。詳しくは前回の記事の実行回数に関する記載についてご確認いただければとお思います。

さて描画がコストのかかる処理だと書きましたが、実際にどれぐらいのコストが掛かっているのでしょうか。ブラウザアプリであれば、開発者ツールから描画に掛かっているコストが確認できます。Chromeであればこんな感じで確認できます。
Chromeの開発者ツールの例

Chromeの開発者ツールの例

モノにもよるとは思いますが、画面表示における大きな割合を占めていることがわかります。

さらにどこが再描画されているのかを知ることができる機能もあります。どれだけ再描画されているか気にされたことがない方は、一度試してみると良いかもしれません。思ったより再描画されている場合、見直した方が良い処理があるかもしれません。

描画は重たい処理であるため、JavaScriptでは昔からFragmentという機能がありますし、Angular、React、Vueといった主流なフレームワークではDOMを仮想化して描画コストを下げようとしています。.NET においてもDataGridという表を表示するコントロールは表示を仮想化しており、表示する行全ての描画はしません。

私がOpenGLを触っていたのは学生時代ですが、CGの描画をチラつかせないためにダブルバッファ法といって、今見せている絵のうらで次の絵を用意しておいて、紙芝居のように表示する絵を入れ替える手法が使われていました。
表示の仮想化の例。見えている行+α だけを描画している。

表示の仮想化の例。見えている行+α だけを描画している。

ダブルバッファ法。2枚の絵を交互に入れ替えて表示する。

ダブルバッファ法。2枚の絵を交互に入れ替えて表示する。

各フレームワーク、開発プラットフォームにおいても描画は重たい処理であるため創意工夫されています。

ではブラウザの描画はどうなっているのでしょうか。ブラウザは Style → Layout → Paint → Compositeという工程を経て描画されています。それぞれの説明についてはGoogleのサイトに説明がありますので、興味があれば確認してみてください。
ブラウザの描画の流れがわかりましたが、描画は1回だけで済むのでしょうか?

先ほど描画回数のお話をしたように何度も描画されるケースはありますが、意図しないタイミングでも描画は発生しえます。

ブラウザの場合、DOM要素へのアクセスやスタイルの変更が発生すると描画発生します。端的にいうと、1回描画してみないと操作したいDOM要素の現状がわからないからです。ブラウザによってどこから描画フローがやり直されるかは違いますが、特定のブラウザだけサポートするようなケースはあまりないと思いますので、差があるという事実以外は知る必要はないかと思ってます。

どのCSSプロパティが描画工程のどれに影響を与えるかを描画エンジン毎にまとめたページがありますので、興味がある方は確認してみてください。多くの描画工程に影響を与えるプロパティは、値変更時のコストが高いことになります。

この事実を知らないと、描画させるような処理を書いてるつもりがなくても結果的に描画させてしまうことがあります。例えば、何かしらのイベントをトリガーとしてあるDOMの大きさなどレイアウトに関する情報を取得しようとした場合、そのイベントが発生するたびに実は描画が発生してしまうかもしれません。この処理をより効率的にしようとする場合、requestAnimationFrameというメソッドを使います。

このメソッドは使い勝手的にはsetTimeoutと同じ感じですが、処理の実行タイミングをブラウザの描画タイミングに合わせてくれます。その結果、非効率な画面描画が減り、より効率的に処理ができるようになります。ただし、ブラウザの別タブを選択して画面が非表示なっているような場合ではrequestAnimationFrameの呼び出し頻度は下がるので、必ず一定頻度で実行したいような場合には利用できません。その点はご注意ください。

1回や2回の処理であればたいした効果はありませんが、秒間に何十回と呼ばれるような処理で利用すると性能が全く変わってきます。Angularの例になりますが、Angularではイベントや非同期処理は全てMicroTaskなどといった処理にラップされるのですが、ngZoneEventCoalescingの機能を使うことで、それらの処理をrequestAnimationFrameで動くようにしてくれます(ただし、執筆時点では問題が起きているため利用には確認が必要です)。また、フレームワーク中で効率的に描画するための仕組みを用意してくれてることもあるので、確認するようにしましょう。
描画回数の次は描画範囲です。再描画する際にどれぐらいの範囲が描画されているのでしょうか。

Chromeであれば開発者ツールからRenderingという項目を選ぶことで、再描画された範囲が画面上に表示されるようになります。

想定以上に画面が描画されている場合、描画範囲を少なくできないか検討する必要があります。ビジュアルツリー/DOMツリーの親の方で描画が走ればその全ての子にも影響与えるので必要なところから再描画が走るようにするべきです。配列を表にバインドして表示する場合も、例えばAngularであればtrackByという機能を使うことで配列の再描画が効率的になります。
緑色の箇所が描画が走っている箇所

緑色の箇所が描画が走っている箇所

仕組み的に描画が遅い機能というものもあります。例えば HTMLのTableタグです。Tableタグで表を描画することがあると思いますが、Tableは各列の幅を自動で調整してくれます。

仕組みを考えてみればわかりますが、調整するためには一回全部の行を確認してみないと調整できません。全部を確認して描画するため、Tableタグは大量データを描画しようとすると非常に重たい処理になってしまいます。

.NETなどであるようなレイアウトの自動調整機能も同様に、直接レイアウトを指定するより自動にした方が処理は重たくなります。便利な機能があっても、そのまま利用して良いのかどうかは考えておく必要があります。もちろん、全てのシーンにおいて性能を考えないといけないわけではありません。

他にもレイアウトの複雑さも描画性能に影響を与えます。HTMLであれば、DOM構造が複雑であればあるほど描画に時間がかかりますし、MVVMモデルのようにViewとそれに紐づくロジックとでアプリを構築している場合は、Viewをネストして表示するほど処理が増えてきます。例えば、XamarinであればLayout Compositionという機能を使うことで、無駄なレンダラーを省いて描画を高速にすることができます。

以上のように、プラットフォームやフレームワークの描画の仕組みやタイミングについて知ることはアプリの性能を考える上で重要なことと言えるでしょう。
▷ メモリを知る
アプリの速度に問題を感じる時に、プロファイラーを使って性能劣化の原因となっている箇所を探すことがあります。先述のようにブラウザであれば、開発者ツールのPerformanceのタブから簡単に性能の測定ができます。

これ以上、ロジックのどこを改善したら良いかわからないこともあると思います。そういう時に、GCの項目が多くなっていることはないでしょうか。

サーバーサイドの性能チューニングで、サーバーに対して大量のリクエストを送り、GCの発生頻度を測定し改善していくような試験をすることがあると思いますが、フロントのアプリでもメモリ管理が自動化されている言語で作っているのあれば同じことことが言えます。

そこで大量のデータをサーバーから受信したり、長時間触って性能が劣化しないか、GCの発生頻度がどれぐらいかを確認します。
この画面の例では処理時間の割合としてGCが3番目になっている

この画面の例では処理時間の割合としてGCが3番目になっている

GCが多く発生しているために性能が劣化している場合、無駄にオブジェクトを生成している箇所がないか確認してみましょう。もちろん1個や2個のオブジェクト生成ではなくて、数百数千というオブジェクトを生成してしまっている処理はないでしょうか。

過去の経験としてあるのは、サーバーから受け取ったレスポンスをパースや、その結果のモデルデータをObservableなオブジェクトでラップ処理するという問題がありました。パースの処理が非効率であったために大量のオブジェクトを作ったり、数十とある項目を全てObservableで持つモデルを数百と生成していました。このように実行回数の多い処理で大量のメモリを利用すると、すぐにGCが頻発するようになります。

この問題がわかりづらいのは、メソッド自体の実行速度は問題ないことがあるからです。プロファイラーで調べても「全てのメソッドは高速に実行されているにも関わらず、全体としてはなぜか遅く、GCが多く発生している」という状態です。

GCが多いため、たくさん存在しているオブジェクトを確認すれば原因がわかるかというと、身に覚えのないオブジェクトがたくさん存在している場合もあり、どれが原因だがわかりません。対策としては地道に処理を見直していくしかありません。

もし、最初は問題ないのに徐々にGCが増えていくというのであればメモリリークが原因かもしれません。メモリリークが発生しているためにメモリを圧迫し、徐々にGCの頻度があがっていくのです。

その場合はもう少し対策は簡単で、メモリのスナップショットを複数回取得し、その差分を確認して増えていっているオブジェクトを確認していきます。ただ、正直に言うとメモリリークに関しては全て解決する必要はなく、そのアプリの利用形態に問題がないレベルであれば良いと思っています。

というのは、最近ではサードパーティのライブラリを利用することも多く、またフレームワーク自体に問題があることもあり、簡単に解決することができないケースもあるからです。たいていのフロントのアプリの連続稼働時間はサーバーよりは短いのでサーバーサイドほど神経質には対策しなくても良いかと思います(決してメモリリークしても良いという意味ではなく、費用対効果として解決する必要がないことがあるということです)。
●まとめ

今回は描画の仕組みを知ることで描画性能が向上できること、メモリを知ることでGCを減らして性能向上ができるという話を解説しました。次回は「処理タイミングを知る」「通信を知る」「ビルドを知る」について書く予定です。
 

株式会社野村総合研究所  福岡ソリューション開発部
上級テクニカルエンジニア
上田 恵

フロントエンドの開発を得意とするエンジニア。主に金融系のアプリ開発を担当。
※記事の内容は個人の見解であり、所属する組織の公式見解ではありません。
twitter facebook このエントリーをはてなブックマークに追加 RSS
【サイトリニューアル!】新サイトはこちらMdNについて

この連載のすべての記事

アクセスランキング

8.30-9.5

MdN BOOKS|デザインの本

Pick upコンテンツ

現在