こんにちわ。
formrunというフォームを簡単に作れてお問い合わせも出来ちゃうサービスを作ってます。
最近問い合わせ管理の機能をSPAでフルリプレイスしました。
当初は遅くてSPAにした意義がまるでなかったのですが、以下に挙げたチューニングを行う事でだいぶパフォーマンスが改善したのでアップします。
問題点
フォームの切り替え、カードの切り替えがAPIの実行などは行なっていないにも関わらず遅い。
改善結果
カード切り替え
before 700ms
after 150ms
フォーム切り替え
before 1200ms
after 430ms
サーバサイド
API
REST API N+1問題
当初はREST APIに従って一つのエンドポイントでは一つのリソースを取得するようにしていました。
ただこの作りだとリクエストのN+1が起きてしまうため極力一つのAPIのエンドポイントでは大きくデータを取得するように変更しました。
formrunではjbuilderを使用してAPIのレスポンスを組み立てていますが、GraphQLを使用してN+1を回避するのもいいと思います。
というか作り直したい。
フロントサイド
フロントボトルネック調査
vue performance devtool
chrome pluginのvue-performance-toolを使用してボトルネックのコンポーネントを調査する方法です。
まずはrecordingをクリックします。
調査を行いたい操作を行いrecodingをストップします。
すると各コンポーネントにかかっている時間がms単位で表示されてるので、Total Timeが大きい箇所から調査していきます。
また各値の意味は下の通りです。
- Init beforeCreated と createdにかかった時間
- Render githubにjsのインスタンスを作成するためにかかった時間とあるので、仮想DOMの生成にかかった時間?
- Patch DOMをレンダリングするためにかかった時間
- Total Time 1つでもNaNmsと出ている箇所があると合計時間がNaNmsになってしまうので各値も確認した方が良いかも
上記のサンプルではCardNewは内包しているDOMの数もそれほど多いわけではないのにRenderに時間がかかっているため優先的に調査していきます。
またカード切り替えとはほぼ関係の無いコンポーネントが再描画されているので、カード切り替えを遅くしている原因の一つだとわかりました。
chrome performance
chromeの開発者ツールにperformance計測のためのツールを使ってボトルネックを調査する方法です。
まずは開発者ツールのperformanceタブを開いて録画ボタンをクリックします。
その後パフォーマンスを確認したい操作を行い録画をストップします。
するとページ下部に以下のような図が出てきます。
横の長さが処理にかかっている時間になっており、縦がスタックトレースになっています。
同じ色の箇所は同じコードの処理になります。
クリックするとページ下部のSummaryのFunctionに該当のソースコードへのリンクが付きます。
このリンクをクリックするよ下記のように表示されます。
これだけだとわかりにくいかもしれませんが、vueファイルのrenderが呼ばれているので、コンポーネントがrenderされている事がわかります。
上記のサンプルではCardNew.vueコンポーネントが何度も呼ばれています。
ぱっと見で確認するべき項目は同じ色が何度も表示されていて、時間がかかっている箇所を優先的に調査していきます。
フロントサイド速度改善
大量コンポーネントのスクロール
大量のコンポーネントを表示してスクロールを行うとスムーズにスクロールが出来ませんでした。
同様の課題を抱えてるvueユーザがvuejsのissueを上げていました。
結論としてはvue-virtual-scrollerを使えという回答でした。
ただvue-virtual-scrollerスクロール対象のコンポーネントの高さが不定な場合に変数で指定出来る仕組みがありますが、カードの中に表示させるラベルが複数指定可能なため、都度高さの計算が必要だったため、導入を諦めました。
state更新による大量のコンポーネントの再描画
formrunではAPIで取得したデータとデータ同士のリレーションをvuex-ormを使用して管理しています。
vuex-ormでは関連するデータの取得にwith withAllを指定してリレーション先のデータを取得するようにしています。
開発当初は必要なリレーション一つ一つを指定していくのが面倒だったため、とりあえずwithAllですべてのリレーション先を取得するようにしていました。
これが意図しないコンポーネントの再描画の原因です。
vuex-ormのサンプルを使用して再現してみます。
モデル構成は以下です。
User -> Todo -> Comment
| |
+-----------------+
Todo一覧ではwithAllを呼び出してTodoのUserとCommentも取得しています。
chromeのperformanceで計測した結果が以下です。
loadCommentしたあとにTodoList.vueが再描画されてます。
TodoList.vueでは一切コメントを表示していませんが、withAllでCommentオブジェクトも取得してしまっているため、Commentを更新したときにTodoList.vueが再描画される原因となっています。
これを修正するためにはcomputedに定義したメソッドで必要なStateのみ取得するように修正します。
修正した結果は以下です。
loadCommentしたあとにupdateComponentが無くなっています。
ちなみにこれはvuex-ormを使わなかった場合でも起きるのでcomputedやgetters内では必要なstateのみを扱うように修正しました。
強制レイアウトの解消
ブラウザがDOMツリーを構築したあとに、レイアウト計算(reflow)と描画があります。
jsの操作によってreflowの回数が増えてしまい描画に時間がかかってしまうためperformanceタブでreflowが存在していたら解消していきます。
SimpleAjaxUploder.jsはファイルアップロード用のライブラリなので、他ライブラリへ乗り換えを検討しています。
このreflowを引き起こす要因は下記プロパティへ参照しただけで起こります。
- offsetTop offsetLeft offsetWidth offsetHeight
- scrollTop scrollLeft scrollWidth scrollHeight
- clientTop clientLeft clientWidth clientHeight
主にポジションの取得のためのプロパティのため、レイアウト計算を行わないとならないためreflowが発生します。
今後やりたい速度改善方法
- マスタに近いデータの永続化
- vue-routerの遅延コンポーネント