Vue functional component advent calendarを始めた背景 ~大量コンポーネント描画時のパフォーマンスを求めて~

この投稿は [Vue functional component Advent Calendar 2019](https://qiita.com/advent-calendar/2019/vue-functional-component) の1日目の記事です

第１日目のこの記事では、Vue functional component advent calendar という特化型Advent calendarをやろうと思った背景について書きたいと思います。

Outline
- 0. これを書いている人(@tomlla)はどんなエンジニアか
- 1. フロントエンドのランタイムフォーマンス問題に直面した
- 2. 解決方法を調べた
- 3. Functional Componentを試して思ったこと


## 0. これを書いている人(@tomlla)はどんなエンジニアか

こんな感じの経験があります. 

- Linuxベースアプライアンス(ネットワーク機器っぽいもの) 
  - about 3 years
  - Linuxのビルド設定をパッケージ入れて、自社で作った機能を入れて、ファームウェアとして作成
  - boot時の設定もろもろ
  - web管理画面
  - 言語とかそういうの: php, c(linux上application) c(NICいじるkernel module), python, vanilla js, sqlite, ...
- Webベースの業務システムとかtoCアプリケーション開発いろいろ
    - about 4 years
    - 対象: 社内向け受発注システム, 不動産関係, 広告の出稿/パフォーマンス管理, シフト管理/人材配置の効率化
    - java8, rails, python(クローラ), postgres, mysql, elasticsearch, vanilla js, vue

経歴を話したいのではなく「フロントエンドだけガリガリ書いてきたわけではない人」がこの記事を書いていることに留意していただければと思います。
つまりこの記事やVue functional component advent calendarで私の書いた内容に対して、強い人からのご指摘/アドバイスをお待ちしております

## 1. フロントエンドのランタイムフォーマンス問題に直面した

ランタイムパフォーマンスという言葉が正しく問題を指している言葉かわかりませんが、ここでいうランタイムとはコンテンツ配信やサブリソースローディング周りの問題ではなく js のscripting timeに関する問題です。

直近でやっている大きな案件が シフト管理/人材配置の効率化 のシステムでして、この案件での問題をご紹介したいと思います。
いわゆる出勤表 みたいなものをwebの画面にだすわけです。
エクセルみたいな表です

|                   | 12/1          | 12/2                 | 12/3                          | ... |
|-------------------|---------------|----------------------|-------------------------------|-----|
| Benedictくん    | 10:00 - 19:00 | デートなので休みます |                               |     |
| Violetさん | 10:00 - 19:00 |                      | 10:00 - 14:00,  20:00 - 24:00 |     |
| ...               |               |                      |                               |     |

縦軸: スタッフ
横軸: 日付

という構造なのですが、なんとケースによっては `横軸31日 * スタッフ数100人` なんですね...
1セル = 1 vue component だとしても 普通にやると 3,000 componentsです!
さらにテーブルの内外には様々な関連情報を表示しています(リアルタイムで人件費計算したりとか)

また画面には[次の月へ] や [前の月へ] みたいな遷移ボタンがあります。

VueのSPAの場合、普通にやると [次の月へ] をクリックすると
1. 現在表示されているコンポーネントに対して Vue.$destroy を実行
2. 遷移先ページのコンポーネントをrendering

という事が行われます

現在は改善してありますが、最悪のエッジケースでは 25秒間画面が固まる(js long task)ということが起きていました.....
(chromeのプロファイラ結果のframe graphを見ると、細かい処理の山が連続しているのではなく、数個の横幅の長い処理が時間を占めていました)

遅い.... こんなもの使い物にならない....

## 2. 解決方法を調べるため我々はジャングルの奥地へと向かった

※ ジャングル = vue関係の勉強会, Vueの本家ドキュメント, Vue forum, Vueの本家Githubリポジトリのissueなど、さらにvue関係なくブラウザのパフォーマンスに関するtips情報色々.


改善の候補として以下のものが上がりました。

- A. そもそも1画面に表示できる最大コンテンツ量を下げる(仕様変更)
  - これは現在も実際の利用データを見ながらビジネス側のメンバーと検討中です。
  - 実際には1ヶ月分の出勤表を俯瞰でチェックしたい、というニーズがあるため、どうしても多くのコンテンツを扱わなければならないことがわかりました。
- B. Functional componentを使う
  - この記事の次のチャプターで説明します。
- C. [Vue virtual scroller](https://github.com/Akryum/vue-virtual-scroller) を使ってviewport内に表示されている部分のみ表示する
  - 詳細は省きますがいくつかの懸念により、スキップしました。確実性の高い D. やE. に時間を当てるためでした。 
  - ただし現在試してみたいと思っています。
- D. 長いjs scriptingを避ける / 長い処理の最中はローディング中などの表示を行う (応答性を高める)
  - 人間の感覚上の「速さ」に貢献したのがこのD.でした。
  - 具体的には以下2つの対策を行いました
      1. 1度に大量のコンポーネントのレンダリングを行わない(defered的な処理を行う)
      2. イベントハンドラの中で長い処理を始める際は、あらかじめvueを使わずにDOMのAPIでローディングUIを表示を行う.  
        Vueを使っているのに直接DOM操作するのは避けたかったのですが、以下の問題が起きていたため仕方なかったのです...
          - vuex stateや `$data` を更新する 
          - → 大規模なComponent re-renderingが始まってしまう   
            (scripting timeが長い vue内部の処理が行われる) 
          - → stateや$data でローディングUIを表示/非表示を行なっているとローディングUIがなかなか表示されない
- E. Vue Componentの再レンダリングやレンダリングタイミングを減らす
    - D. の次に効果があったのがこのE.でした。具体的には以下の変更を行いました。
        - 巨大なコンポーネント内で直接htmlレンダリングをしない.  
          なぜならごく小さな一箇所の変更であってもコンポーネント全体のレンダリング処理が行われるため。
          変更が多い部分はコンポーネント化して再レンダリングの局所化を目指しました。
        - templateで参照されている `state/$data` の変更タイミングに注意する。  
            - 例:  `this.dataA, this.dataB, ... this.dataX` および これらを参照している `Computed propertyZ` があるとします。  
            - 問題点: API-Aを呼び出してレスポンスデータをdadaAをセット, API-BをdataBに... とやっていると都度レンダリングが走ってしまいます。 
            - 解決法:   
              `Computed propertyZ` ではなく、`this.computedDataZ` というレンダリング用state dataを用意し、全てのデータがAPIから取得できた後に`this.computedDataZ`に値をセットしました。これでレンダリング回数が1回になるわけです。(もっと綺麗な方法を見つけたい)



## 3. パフォーマンス改善としてFunctional componentを使うアプローチの感想

**そもそもFunctional componentとは何でしょうか?**
Functional componentを知らない人のために紹介します。
私はVue の Functional componentは 「Stateless component」という名前がふさわしいと思います。

まずFunctional componentではない普通のVueインスタンスは以下の点がFunctional componentと違います。

- 普通のVueコンポーネントはインスタンス化される
  - インスタンス単位でComputed propertyの結果がキャッシュされるので不要な再計算がスキップできる
- 普通のVueコンポーネントはライフサイクルをもつ
  - 参考: https://jp.vuejs.org/v2/guide/instance.html#%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B5%E3%82%A4%E3%82%AF%E3%83%AB%E3%83%80%E3%82%A4%E3%82%A2%E3%82%B0%E3%83%A9%E3%83%A0

逆にいうとFunctional componentは以下のことができません。

- Computed propertyが使えない
    - インスタンスが無いのでインスタンス単位での結果のキャッシュを持てない
- インスタンスを持っていないためにthisが使えない
    - つまりコードの記述は通常のコンポーネントと大きく変わる
    - jsコード内でもtemplate内でも使えない. methodsとかplugin/mixinも使えない

一方Functional Componentには以下のメリットがあります

- インスタンス生成時にやっている処理をスキップできる
- ライフサイクル変化の際の処理をスキップできる。


**実際のコード変更時・変更後の感想**

まずFunctional Componentを使って、確かに初回レンダリングは **とても早くなりました!**
一方、コードの変更は結構大きいものでした。変更工数と得られるパフォーマンスのコスパを考えると無条件におすすめできるものではありません。
ということでこの辺の現場感のある知見を [Vue functional component advent calendar](https://qiita.com/advent-calendar/2019/vue-functional-component) で紹介して行きたいと思います!