vuejsで作ったSPAの速度改善方法

こんにちわ。
[formrun](https://form.run/ja)というフォームを簡単に作れてお問い合わせも出来ちゃうサービスを作ってます。
最近問い合わせ管理の機能をSPAでフルリプレイスしました。
当初は遅くてSPAにした意義がまるでなかったのですが、以下に挙げたチューニングを行う事でだいぶパフォーマンスが改善したのでアップします。

# 問題点
フォームの切り替え、カードの切り替えがAPIの実行などは行なっていないにも関わらず遅い。

# 改善結果
### カード切り替え
before 700ms
after 150ms

### フォーム切り替え
before 1200ms
after 430ms

# サーバサイド
## API
### REST API N+1問題
当初はREST APIに従って一つのエンドポイントでは一つのリソースを取得するようにしていました。
ただこの作りだとリクエストの[N+1](https://restfulapi.net/rest-api-n-1-problem/ "N+1")が起きてしまうため極力一つのAPIのエンドポイントでは大きくデータを取得するように変更しました。

![スクリーンショット_2018-07-03_1_17_07.png](https://qiita-image-store.s3.amazonaws.com/0/35451/2ea639e0-e6d3-1201-1c1e-67c7b63c6caa.png)

formrunではjbuilderを使用してAPIのレスポンスを組み立てていますが、GraphQLを使用してN+1を回避するのもいいと思います。
というか作り直したい。

# フロントサイド

## フロントボトルネック調査

### vue performance devtool
chrome pluginの[vue-performance-tool](vue-performance-tool https://chrome.google.com/webstore/detail/vue-performance-devtool/koljilikekcjfeecjefimopfffhkjbne?hl=ja )を使用してボトルネックのコンポーネントを調査する方法です。
![vue_performance.gif](https://qiita-image-store.s3.amazonaws.com/0/35451/e5fa6f32-ee7d-3ba5-88bf-b785331c5eb3.gif)

まずは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計測のためのツールを使ってボトルネックを調査する方法です。
![chrome_performance.gif](https://qiita-image-store.s3.amazonaws.com/0/35451/fd16f740-4028-7c09-ca9e-917d8a49fee2.gif)

まずは開発者ツールのperformanceタブを開いて録画ボタンをクリックします。
その後パフォーマンスを確認したい操作を行い録画をストップします。
するとページ下部に以下のような図が出てきます。

![スクリーンショット_2018-07-03_3_23_32.png](https://qiita-image-store.s3.amazonaws.com/0/35451/5a3fb63b-0d15-05b9-90b3-a175c023bff7.png)

横の長さが処理にかかっている時間になっており、縦がスタックトレースになっています。
同じ色の箇所は同じコードの処理になります。
クリックするとページ下部のSummaryのFunctionに該当のソースコードへのリンクが付きます。
このリンクをクリックするよ下記のように表示されます。

![スクリーンショット 2018-07-03 3.55.31.png](https://qiita-image-store.s3.amazonaws.com/0/35451/f23c07be-dc22-d533-dd0d-9a2eed3cce5c.png)

これだけだとわかりにくいかもしれませんが、vueファイルのrenderが呼ばれているので、コンポーネントがrenderされている事がわかります。
上記のサンプルではCardNew.vueコンポーネントが何度も呼ばれています。
ぱっと見で確認するべき項目は同じ色が何度も表示されていて、時間がかかっている箇所を優先的に調査していきます。

## フロントサイド速度改善

### 大量コンポーネントのスクロール
大量のコンポーネントを表示してスクロールを行うとスムーズにスクロールが出来ませんでした。
同様の課題を抱えてるvueユーザがvuejsの[issue](issue https://github.com/vuejs/vue/issues/2000)を上げていました。
結論としては[vue-virtual-scroller](vue-virtual-scroller https://github.com/Akryum/vue-virtual-scroller)を使えという回答でした。

ただvue-virtual-scrollerスクロール対象のコンポーネントの高さが不定な場合に変数で指定出来る仕組みがありますが、カードの中に表示させるラベルが複数指定可能なため、都度高さの計算が必要だったため、導入を諦めました。

### state更新による大量のコンポーネントの再描画
formrunではAPIで取得したデータとデータ同士のリレーションを[vuex-orm](https://github.com/vuex-orm/vuex-orm)を使用して管理しています。
[vuex-orm](https://github.com/vuex-orm/vuex-orm)では関連するデータの取得にwith withAllを指定してリレーション先のデータを取得するようにしています。
開発当初は必要なリレーション一つ一つを指定していくのが面倒だったため、とりあえずwithAllですべてのリレーション先を取得するようにしていました。
これが意図しないコンポーネントの再描画の原因です。
[vuex-ormのサンプル](https://github.com/vuex-orm/vuex-orm-examples)を使用して再現してみます。

サンプルはよくあるTODOです。
![スクリーンショット_2018-07-03_20_12_04.png](https://qiita-image-store.s3.amazonaws.com/0/35451/c5d38c68-c989-81bf-884a-7b87c2dca7d9.png)

モデル構成は以下です。

```
User -> Todo -> Comment
 |                 |
 +-----------------+
```

Todo一覧ではwithAllを呼び出してTodoのUserとCommentも取得しています。
![スクリーンショット_2018-07-03_20_12_04.png](https://qiita-image-store.s3.amazonaws.com/0/35451/ad73f309-1b87-bacb-4172-1192df7169b1.png)

loadCommentボタンでコメント情報を更新します。
![スクリーンショット_2018-07-03_20_12_04.png](https://qiita-image-store.s3.amazonaws.com/0/35451/3a37cc5e-c5af-7307-4a56-a94a49d21be2.png)

chromeのperformanceで計測した結果が以下です。
![スクリーンショット_2018-07-03_20_28_06.png](https://qiita-image-store.s3.amazonaws.com/0/35451/aaab8edc-69de-78d8-f8a6-50f27f435add.png)

loadCommentしたあとにTodoList.vueが再描画されてます。
TodoList.vueでは一切コメントを表示していませんが、withAllでCommentオブジェクトも取得してしまっているため、Commentを更新したときにTodoList.vueが再描画される原因となっています。
これを修正するためにはcomputedに定義したメソッドで必要なStateのみ取得するように修正します。
修正した結果は以下です。

![スクリーンショット_2018-07-03_20_41_37.png](https://qiita-image-store.s3.amazonaws.com/0/35451/83150f64-69f1-5fd8-38fc-2262c9ae6da6.png)

loadCommentしたあとにupdateComponentが無くなっています。
ちなみにこれはvuex-ormを使わなかった場合でも起きるのでcomputedやgetters内では必要なstateのみを扱うように修正しました。

### 強制レイアウトの解消
[参考資料](https://developers.google.com/web/tools/chrome-devtools/rendering-tools/forced-synchronous-layouts?hl=ja)

ブラウザがDOMツリーを構築したあとに、レイアウト計算（reflow）と描画があります。
jsの操作によってreflowの回数が増えてしまい描画に時間がかかってしまうためperformanceタブでreflowが存在していたら解消していきます。

![スクリーンショット_2018-07-03_23_11_30.png](https://qiita-image-store.s3.amazonaws.com/0/35451/cea10572-3441-38b7-8202-fa3725eb0b09.png)

SimpleAjaxUploder.jsはファイルアップロード用のライブラリなので、他ライブラリへ乗り換えを検討しています。
このreflowを引き起こす要因は下記プロパティへ参照しただけで起こります。

- offsetTop offsetLeft offsetWidth offsetHeight
- scrollTop scrollLeft scrollWidth scrollHeight
- clientTop clientLeft clientWidth clientHeight

主にポジションの取得のためのプロパティのため、レイアウト計算を行わないとならないためreflowが発生します。

## 今後やりたい速度改善方法
1. マスタに近いデータの永続化
2. vue-routerの遅延コンポーネント