ウェブフロントエンドのパフォーマンス改善のひとつの日常

ひとつの日常

この間、仕事でちょっとしたハイブリッドアプリ(ネイティブアプリのWebViewの上で動くWebアプリ、とここでは定義しておく)をリリースした。 そのアプリにはタブがあって、(当たり前だが)タブでコンテンツが切り替わる。

リリースして3日くらい経って、企画の人から「なんか、ここのタブ、反応悪くない?」という話が来た。たしかにタブに触れても、すぐには反応しなくて1~2秒経ってからタブが切り替わる。しかも、切り替えたタブのコンテンツが表示されるのにも1~2秒かかっている、うへー... 改善するぞ!

ということは、クライアントの開発をおこなっていると、ちょくちょくあるのではなかろうか。意識、あるいは無意識におこなっている改善の手順について、先の問題の改善に至るまでの具体的は話、ツールも含めながら書く。

計測する

「計測するまでは速度のための調整をしてはならない」という有名な格言がある。自分が書いたコードであれば、「反応が悪い」と聞くと直感でいくつか原因と思しきものが閃く。閃くと、すぐにコードに手をいれたくなる。その欲求に耐える。 原因と思しきものを手当たり次第に修正しても、実際には全くの的はずれで改善されない、仮に改善されたとしても、本来不要だったコードの修正作業によって無駄に時間をかけてしまう。コスパが悪い。計測する、という作業は一見遠回りに見えるけれども、長い目で見たら近道。

再現させる

今回の問題は開発環境では起きず、本番環境だけで起きている。明らかな違いはAPIのレスポンスデータのサイズや内容である。タブの反応が悪い状況を開発環境でも再現させるために、Debugging Proxyと呼ばれるカテゴリのツールを活用した。自分が利用しているのはCharles。他にもFiddlerというのが有名らしい。まず、端末でプロキシの設定をおこなって、アプリを操作、本番のアプリのAPIレスポンスをファイルとして保存する。その後に開発用のアプリで呼び出しているAPIレスポンスを先ほど保存したファイルの内容に差し替える(Local Proxy)、といった手順。その過程で、本番のアプリケーションAPIの呼び出しにかかっている時間を調べることもできる。

計測のための仕込み

ウェブフロントエンドで計測に使える道具はいくつかある。手軽なのは、JavaScriptのUser Timing API。詳しい使い方は User Timing API: Understanding your Web App - HTML5 Rocks を参照してもらいたいが、プログラムでマークを付けて、マーク間の時間を取得できる。supportしていないBrowserにはPolyfill( GitHub - nicjansma/usertiming.js: UserTiming polyfill )もある。

今回の場合は、タブに触れたタイミングで呼ばれるイベントリスナーとDOM操作が確実に完了しているタイミングで呼ばれるコールバックでmarkをつけた。さらに原因を絞り込む、特定するために、その間の通信、レスポンスデータの整形、DOM操作のトリガーとなる処理(MVVMライブラリのViewModelのデータの変更)にもmarkをつけた。

計測

何回か画面を操作してmark間の時間を計測する。自分は統計の知識が正直疎いのでこれが良いやり方かどうかは分からないが、15~20回繰り返して、最良の結果と最悪の結果を破棄した上で平均値をみることにした。※

その結果、DOM操作のトリガーとなる処理からDOM操作が完了するまでの間にボトルネックがあることが分かった。自分の場合はここで、本番環境のAPIのレスポンスが、開発環境のAPIのレスポンスにはない特徴があり、それをそのまま利用して、ViewModelのデータを変更することが問題だと特定した。

※ 今回は時間を指標にしているが、fpsの分散やメモリの使用量が指標になるときもあると思う

Chrome DevTools、さらに深く

今回は利用しなかったが、Chrome DevToolsを使えばBrowserの中で起きていることを詳しく計測できる。 例えば、通信の内容、JavaScriptの関数単位での実行時間、JavaScriptエンジンのメモリ使用量の推移、GCが走ったタイミングの把握、レンダリングに関係したイベントやそれにかかった時間、などなど。

DevToolsの使い方について網羅的に知りたい場合は、WEB+DB PRESS Vol.89 の特集が良いです。 (DevToolsは日々機能が追加されたり、UIが変わるので、既に特集の説明と実際の画面が異なる部分もあるかも)

WEB+DB PRESS Vol.89

WEB+DB PRESS Vol.89

最近はRemote Debugの機能が充実しているので、PCのウェブページ・ウェブアプリに限らず、スマホのウェブページ・ウェブアプリでもDevToolsを利用できる。

ただ、最初からこのような項目の計測に走ると「木を見て森を見ず」という過ちを犯す可能性があるので、まずはUser Timing APIで、ざっくり計測してボトルネックがありそうなところはなんとなく分かったが、それでも明確に原因が分からない場合、自分の仮説の裏付けが欲しいという場合に利用する。

目標を定める

原因を特定したら、改善にあたって目標を定める。例えば「これまで平均 X msかかっていたものを平均 Y msにすれば、画面を操作している上で感じていた違和感をなくすことができるだろう」といった様に。このような目標を定める理由として、必要以上にズルズル時間をかけないようにするため。

改善のためにコードに手を入れる

改善のためにコードに手をいれる。自分のケースの場合は、利用しているMVVMライブラリのAPIドキュメントを読んで、リストレンダリングを最適化するようにしたり、データを細かく分割してUIスレッドをブロック(画面に触れても反応しない現象)しないようにした。
キャッシュする、バッファする、アルゴリズムを変える、データ構造を変える。問題によって、それにふさわしいアプローチがあるはず(ここらへん、もっと鍛えたい)。

上で述べた内容はライブラリ利用者として完結したコードの修正だが、ライブラリ本体のパフォーマンス改善に貢献する、という道もあるはず。

再び計測する

改善をおこなったら、また計測する。画面を操作することで改善したかどうか分かるが、それでも仮説が合っていたか、目標に達したか調べるために数値を出す。ダメだったら、原因と特定したものが誤っていた、コードの手の入れ方がまずかったということなので、やり直す。

改善の結果、まとめ

とある日常の出来事を取り上げながら、ウェブフロントエンドのパフォーマンス改善の手順について紹介しました。

結果として、アプリはサクサク動くようになり、企画の人や一緒に働いているサーバサイドのエンジニアの人に「kitak sakusaku special」というよく分からないお言葉を頂いて、めでたしめでたしだったのでした。

個人的には計測した結果のデータをどう見るか、捉えるか、特定した原因にどのようなアプローチを取るか、どのようにコードを修正するか、といった部分を鍛えていくことが今後の課題だと感じた次第です。