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

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

2020.10.28 WED

[優れたUXを目指して]アプリの性能について:第3回
前回に引き続き、アプリの性能について考えておくべき話を解説していきます。前回は描画とメモリについて触れましたが、今回は処理タイミング、通信、ビルドについてお話ししたいと思います。

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

TEXT: 2020年7月6日 上田恵
▷ 処理タイミングを知る
処理タイミングの一つ目の話はファイルのロードタイミングです。アプリを起動する際、いろいろなファイルをロードすると思いますが、適切なタイミングでロードすることで起動速度を最適化できます。

例えば、Webであれば CSSやJSファイルの読み込みタイミングになります。ブラウザでWebサイトを開いた際、ブラウザは返ってきたHTMLを上から順番に処理していきますが、その途中で重たい読み込み処理があるとそこで画面の描画が止まってしまいます。そのため、今では慣習のように書いてますが、JSファイルはHTMLの最後の方でロードすることが多いかと思います。最初に全てHTMLを描画させてしまい、後からJSファイルを読み込み、処理を開始するためです。さらにCSSや画像などにはpreloadという指定をすることができます。

こうすることで優先的にダウンロードするようにブラウザに指示できるので、状況によるとは思いますが初期画面の表示にかかる時間を最適化できる可能性があります。ただし、preloadのブラウザサポートは完璧ではないので注意が必要です。

JSファイルも読み込みタイミングを調整可能です。最近のWebアプリ開発では、巨大な1個のJSファイルを作らずに複数のJSファイルまたはTSファイルをwebpackのようなバンドラーで1個にまとめていると思います。このまとめ方も1個にせずに、必要な単位で分割することができます。

ルーティングの単位で分割したり、もしくはユーザーがよく使う機能・画面が最初から想定できるのであればよく使う機能だけは最初に読み込んでおいて、あまり使われない機能は必要な時に読み込むようにするなど、色々な戦略が考えられます。例えばAngularのルーティング機能であれば暇な時に先読みするような機能もあります。

ファイルの単位は更新頻度も考慮すると良いかもしれません。よくあるのは、滅多に更新しないサードパーティ製のライブラリやフレームワーク等だけ別ファイルにまとめておくことで、何かしらのアプリ修正が入ったとしてもサードパーティのJSファイルだけはブラウザのキャッシュを利用して読み込むのでダウンロード時間に影響を与えないという手法です。
ネットワークの処理順や所要時間を確認しておく必要がある

ネットワークの処理順や所要時間を確認しておく必要がある

処理タイミングの 2 つ目の話は非同期処理についてです。非同期処理とは処理の呼び出しと結果の受け取りが異なるタイミングで実行される処理のことで、HTTP通信をする時にPromiseやAsync/Awaitを使った処理を書いた経験がある方も多いと思います。このAsync/Awaitですが、Webアプリではシングルスレッドなので意識しませんがマルチスレッドで動作する場合はどのスレッドで動くのかを気にしておく必要があります。TypeScriptで書いたWebアプリのAsync/Awaitの動きと、C#で書いたネイティブアプリのAsync/Awaitの動きは中身としては全く違います。

マルチスレッドの場合、非同期処理の戻りをコールバック(Promise相当)で受け取るのかAsync/Awaitで受け取るのかで動きが異なってきます。コールバックのスレッドはアプリの作り次第ですが、Async/Awaitの場合は言語・プラットフォームの仕様次第と思いますが同じスレッドで返ってくるのではないかと思います。
下記2つのパターンでSomeProcessがどのスレッドで実行されるのか気にしておく必要がある

 SomeFunction((result) => {     
   SomeProcess(); 
 });


 var result = await SomeFunction(); 
 SomeProcess();   
アプリケーションの作りによると思いますが、場合によってはAsync/Awaitを使う場合でも別スレッドで返ってくるようにした方が早くなることもあります。C#であればTask.ConfigureAwaitで制御できます。もちろん、呼び出す前から別のスレッドに切り替えておけば良いのかもしれませんが、マルチスレッドで動く場合はどの処理がどのスレッドで動いているのかを意識しておく必要があります。性能への影響はもちろん、デッドロックの危険性もあります。実際にAsync/Awaitの処理でも簡単にデッドロックを起こせます。

マルチスレッド動く環境であれば、どの処理をメインスレッドで、どの処理を他のスレッドでやるのか、どこでスレッドを切り替えるのかを考える必要があります。ソケットを監視する処理のように何かを監視するような処理や、時間のかかる処理は専用のスレッドで実行するようにします。HTTP通信の応答の処理も、パースしてデータを整形するような処理までは別スレッドで実行し、ViewやViewModelに処理を戻す直前にメインスレッドに戻したりします。

前回、前々回と実行回数や描画のタイミングなど多くのタイミングに関する話が出てきました。性能を考える上では何が起きているのかを認識する必要があるということです。
▷ 通信を知る
通信もまた性能に大きな影響を与えます。極端に通信回数が多かったり、大きなサイズのデータをダウンロードしたりすると当然、性能は劣化します。

Chromeであれば同一ホストの同時接続数は「6」です。つまり、7回以上Ajaxで通信しようとすると7個目からは他の通信が終了するのを待たされることになります。開発者ツールのNetwork画面でどれだけ通信しているのかを確認する必要があります。実行回数の話で書いたような、想定以上に処理が実行されてしまっている場合は処理を見直しましょう。多くの場合、機能や要件の設計書を見てテストケースを作りテスターがテストを実施すると思いますが、機能要件通りのテストした場合はこの問題には気づけないので開発者が意識しておく必要があります。

通信回数が多くて性能を劣化させている原因が実装上の問題でなければ、それは設計の問題になります。例えば、1画面内に表示する情報を取得するためのAPIがたくさんある場合などです。この問題を回避するには、実装に入る前の設計の段階で意識しておく必要があります。要件やDBや外部システムのデータ構造からサーバーのAPIと画面の設計を決めておく必要があります。極論、1画面1つのAPIにしておけば問題は回避されますが、拡張性としてあまり良いとは言えません。最近のデバイスやネットワークは十分高速なのでそこまで神経質にAPIをまとめる必要はないので、ある程度意味のある単位でAPIをまとめると良いかと思います。他にも GraphQLを使うというのも1つの対応だと思います。

データをダウンロードするとレスポンスをパースすると思います。パースもまた性能に与える影響は小さくありません。例えばデータがJSON形式である場合、JavaScriptであればあまり意識しませんが、その他の環境ではJSONのパーサーを用意します。自前でパーサーを作る場合は性能を十分に考える必要があります。

パースは繰り返し処理の集合体のようなものなので、1回の処理が0.1msecだとしても、それが1000回呼ばれると100msecになってしまうからです。無駄なオブジェクトの生成が多いと、生成コストもかかりますし、GCの頻度にも影響を与えるので性能に影響を与えます。リソースが潤沢でない場合は、オブジェクトの使い回しやリングバッファを使うなどの工夫が必要になってくるかもしれません。

こういったコアなデータ処理が遅いと画面表示が固まったように見えるので著しくUXを損ないます。画面のロジックはわかりやすさ優先で良いと思いますが、コア機能についてはわかりやすさよりも性能を優先した方が良いでしょう。
▷ ビルドを知る
最後はビルドの話です。プログラムを書いたら最後はビルドしてアプリを配布可能な状態にしますが、そこでもいろんな工夫がされています。

WebアプリであればTree Shakingとminifyがあります。Tree Shakingとは利用されていないコードを除去する機能です。不要なコードを削除することでファイルサイズを小さくします。minifyはファイルから空白や改行を除去したり、変数名を短くしたりロジックを短くしたり、など色々な方法で記述内容を減らしてファイルサイズを小さくしてくれます。環境によってはProductionのビルドをすると自動的にしてくれるので意識していない人もいるかもしれませんが、一から開発する場合は必ずチェックしておかないといけないことなので知っておく必要があります。

他にも AOT(Ahead-of-time)という機能があります。これはアプリ動作時に実行・解釈している処理をアプリコンパイル時に実行する機能です。AOTにすると一般的には生成されるファイルサイズが大きくなりますが、処理が効率化されるため性能は向上します。

この仕組みは、その環境で直接動くようなバイナリでない限りは、何かしら独自の構文で処理ステップが書かれており、それを解釈しながら、より低レイヤーな命令に変換しています。AOTとはその変換処理を事前にやってしまうことです。一般的にはコンパイル時に変換してしまうとコンパイル結果のファイルは大きくなります。ファイルが大きくなるデメリットよりも性能向上によるメリットの方が大きい場合にAOTを利用します。

しかし、必ずファイルが大きくなるわけではなく、例えば AngularであればAOTを使うと、Webアプリ実行時にAngularのランタイムが不要となるために結果的にはファイルサイズが小さくなる、といったこともあります。ここでいうAngularのランタイムとは、Angularの構文で書かれた処理を解釈する機能のことを指します。Angularのランタイムはかなりファイルサイズが大きいため、AOTによるファイルサイズ肥大化よりもランタイムをなくす方が最終的なファイルサイズが小さくなる可能性があるのです。実際、よっぽど巨大な WEB アプリでない限りは AOTにした方が軽くなるはずです。

ちなみに他のプラットフォームの話をするとAndroidの場合はJIT/AOTの両方が動きますが、iOSについてはシミューレーターはJITですが、実機はAOTでないと動かないようになっています。

このように利用するプラットフォーム、フレームワーク毎に様々な手段が用意されています。コンパイラには私のような常人には理解できないレベルでいろんな工夫がされていますので、変な小細工は考えず最適化周りはコンパイラに任せるというのも手です。

例えば、私が学生時代はgccでC++をコンパイルして研究用のプログラムを実行していました。AOT/JITの話でいうとAOTになります。gccには最適化オプションがあるのですが、最適化オプションをつけると劇的に処理速度が早くなりました。中身としては関数のインライン展開をしていたりします。関数呼び出しをするためには、レジスタの値をスタックに積んで関数のプログラムまでジャンプするという処理がアセンブラレベルでは実行されており、関数呼び出しをなくすだけでもそれらの処理ステップが省けて高速になるという仕組みでした。

ただし、何でもかんでもインライン展開すれば良いと言うわけではないらしく、インライン展開するべきかどうかをヒューリスティックに判定していると聞いたことがあります。この話自体はかなり昔の話なので今でも通用する話かはわかりませんが、いずれにせよ人類の叡智が詰まっていると言っても過言ではなく、小手先の工夫をするよりもコンパイラに任せる方が無難なことが多いでしょう。

利用する開発環境やフレームワークなどにどういったコンパイル機能が用意されているかは知っておいても損はないと思います。
●まとめ

このシリーズは今回のお話で終了となります。全体としてはそこまで細かい話はせずに概要としてお伝えしました。それは実際に性能対策をする場合は状況によって手段が変わってくるため、解決の糸口となるヒントをなるべく多く書くことに注力したためです。もし困ったときにこの記事が助けになれば幸いです。

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

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

この連載のすべての記事

アクセスランキング

10.19-10.25

MdN BOOKS|デザインの本

Pick upコンテンツ

現在