こんにちは!フリーランスエンジニアとしてスマートキャンプに参画している芳岡です。
弊社のプロダクトであるBiscuet(https://biscuet.jp/)の開発に初期から参画していますが、サービスが世の中に展開されていく過程、チームが大きくなっていく過程を間近で見れとても興味深く思っています。
今回は、そのBiscuetで使用しているVue.jsのパフォーマンス改善を行ったのですが、そこで気づいたいくつかのポイントを整理してお届けします。
Vue.jsのパフォーマンス
Vue.jsでは、状態変更によって、仮想DOMが繰り返し再計算(updateRender
)されるため、それによりパフォーマンスが悪くなることがあります。
再計算に百msもかかってしまうと、もっさりした動作となりユーザにも印象がよくありません。
そこで、再計算の回数、つまりVue.jsのライフサイクルでいうところのupdate
の回数を減らして改善を試みます。
ポイントは以下の2点となるのでそれぞれ説明していきます。
- 関数型コンポーネント
props
は、オブジェクト全体か各プロパティごとか?
関数型コンポーネント
関数型コンポーネントについての公式な解説は、こちらをご覧ください。
通常のコンポーネントと関数型コンポーネントを比較するため、千個のボタンコンポーネントを並べました。
このボタンを関数型コンポーネントにした時の表示時の処理速度を比較したいと思います。
結果はこちら
クリックしたら描画されるようにしたので、クリックイベントの時間を見ています。
通常のコンポーネントが153ms
かかっているのが、関数型コンポーネントで71ms
になりました。約半分ですね。
ボタン一個あたりだと、82μs
の短縮です! まあ、μsを体感できる方はいないと思いますが...。
ひとつひとつでは小さな差ですが、ボタンだけでなくアイコンや、コンテナの機能を持つコンポーネントなど、出来るだけたくさんのコンポーネントを関数型コンポーネントとすれば、塵も積もれば山となりパフォーマンスアップに繋がっていきます。
最後に、それぞれのボタンコンポーネントのコードです。
ノーマルなコンポーネント
<template> <button class="btn" @click="click" :style="styles"> {{ label }} </button> </template> <script> export default { props: { label: String, color: String }, computed: { styles() { return { "background-color": this.color }; } }, methods: { click() { this.$emit("click"); } } }; </script> <style scoped> .btn { width: 50px; } </style>
関数型コンポーネント
<template functional> <button class="btn" :style="$options.styles(props)" v-on="listeners"> {{ props.label }} </button> </template> <script> export default { props: { label: String, color: String }, styles(props) { return { "background-color": props.color }; } }; </script> <style scoped> .btn { width: 50px; } </style>
差分は以下です。
template
タグに、functional
属性を付ける。props
で渡された値は、props.プロパティ名
で呼び出す。computed
、methods
はなくなり、直接関数を定義する必要がある。また、定義した関数は$options.関数名
で呼び出す。下位コンポーネントから上位のコンポーネントへイベントを上げるため、
v-on="listeners"
を設定する。
ここでは、例として挙げたため、props
の数が2つですが、props
の数が多いほうが、処理時間の差は大きくなります。
propsは、オブジェクト全体か各プロパティごとか?
親から子コンポーネントに、props
を通して、値を渡すときどのようにしてますか? オブジェクトで渡してますか、それともオブジェクトのプロパティを個別に渡していますか?
やり方によってはupdate
の回数に大きく差がでる場合があります。
以下のコードではパフォーマンスの差を確認するために300個のボタンを並べてます。
プロパティごとに渡す方法
親コンポーネントのコード
<template> <div> <div> <p> <button @click="changeProperty">プロパティ変更</button> | <button @click="changeObjectRef">参照変更</button> </p> <p> オブジェクト:{{ obj }} </p> </div> <normal-btn :label="aObject.label" v-for="i in list" :key="`a-${i}`" /> <normal-btn :label="bObject.label" v-for="i in list" :key="`b-${i}`" /> <normal-btn :label="cObject.label" v-for="i in list" :key="`c-${i}`" /> </div> </template> <script> import NormalBtn from "@/components/NormalBtn"; export default { components: { NormalBtn }, data() { return { list: [...Array(100).keys()], obj: { a: { deep1: { deep2: 1 } }, b: 2, c: 3 } }; }, computed: { aObject() { return { label: this.obj.a.deep1.deep2.toString() }; }, bObject() { return { label: this.obj.b.toString() }; }, cObject() { return { label: this.obj.c.toString() }; } }, methods: { changeProperty() { this.obj.a.deep1.deep2 = this.obj.a.deep1.deep2 + 1; }, changeObjectRef() { this.obj = Object.assign({}, this.obj); } } }; </script>
子コンポーネント(NormalBtn)のコード
<template> <button class="btn" @click="click" :style="styles"> {{ label }} </button> </template> <script> export default { props: { label: String, color: String }, computed: { styles() { return { "background-color": this.color }; } }, methods: { click() { this.$emit("click"); } } }; </script> <style scoped> .btn { width: 50px; } </style>
親側で、ひとつの大元のオブジェクト(変数名:object
)から、computed
でaObject, bObject, cObjectという派生したオブジェクトを作成してます。
この派生したオブジェクトのプロパティ(String
)を子コンポーネントに渡しています。
さて、ここで大元のオブジェクト(変数名:object
)を変更したらどのようになるでしょうか。子コンポーネントはupdate
されますか?
答えは、props
に渡された値が変更された時だけ、その子コンポーネントがupdate
されます。逆に、props
に変更がない子コンポーネントはupdate
されません。
変数object
の参照をまるっと入れ替えても、同じプロパティ値であればupdate
されません。(なお、プロパティ値が同じでも、参照が入れ替わった場合は、親コンポーネントのcomputed
は再計算されるので注意してください)
オブジェクトを渡す方法
次に親コンポーネントから子コンポーネントにオブジェクトを渡す場合です。
親コンポーネントのコード
<template> <div> <div> <p> <button @click="changeProperty">プロパティ変更</button> | <button @click="changeObjectRef">参照変更</button> </p> <p> オブジェクト:{{ obj }} </p> </div> <object-prop-btn :object="aObject" v-for="i in list" :key="`a-${i}`" /> <object-prop-btn :object="bObject" v-for="i in list" :key="`b-${i}`" /> <object-prop-btn :object="cObject" v-for="i in list" :key="`c-${i}`" /> </div> </template> <script> import ObjectPropBtn from "@/components/ObjectPropBtn"; export default { components: { ObjectPropBtn }, data() { return { list: [...Array(100).keys()], obj: { a: { deep1: { deep2: 1 } }, b: 2, c: 3 } }; }, computed: { aObject() { return { label: this.obj.a.deep1.deep2.toString() }; }, bObject() { return { label: this.obj.b.toString() }; }, cObject() { return { label: this.obj.c.toString() }; } }, methods: { changeProperty() { this.obj.a.deep1.deep2 = this.obj.a.deep1.deep2 + 1; }, changeObjectRef() { this.obj = Object.assign({}, this.obj); } } }; </script>
子コンポーネント(ObjectPropBtn.vue
)のコード
<template> <button class="btn" @click="click" :style="styles"> {{ object.label }} </button> </template> <script> export default { props: { object: Object, color: String }, computed: { styles() { return { "background-color": this.color }; } }, methods: { click() { this.$emit("click"); } } }; </script> <style scoped> .btn { width: 50px; } </style>
先に示した「プロパティごとに渡す方法」のコードとほぼ同じです。
違いは、子コンポーネントに渡す値をオブジェクトのプロパティでなく、オブジェクト全体を渡しているところ、子コンポーネントでは、受け取ったオプジェクトのプロパティを使用しているところです。
再度、同じ質問です。ここで大元のオブジェクト(変数名:object
)を変更したら、どのようになるでしょうか。子コンポーネントはupdate
されますか?
答えは、子コンポーネントで使用するオブジェクトのプロパティが変更されると、その子コンポーネントがupdate
されます。
使用するプロパティの値が同じなら、子コンポーネントはupdate
されません。
「プロパティごとに渡す方法」との違いは、変数object
の参照が変わった場合です。
内部のプロパティが全て同じであっても関連する子コンポーネントが全てupdate
されます! この例でいえば、300個のボタン全て(値が変更されていないものも含め)にupdate
が走ります。
注釈
上で述べたように「オブジェクトを渡す方法」だと不要なupdate
が行われる可能性があります。
ですが、言いたいことは「オブジェクトを渡すな」ということではなく、 実際の案件は複雑で、このような事例が紛れ込み易く見つけづらいということです。
Biscuetでも、オブジェクトを子コンポーネント、さらには孫コンポーネントに渡して操作したり、間にstore
を通してデータを変更したりしています。
具体的な修正の方法は様々だと思います(object
の参照を変えないようなロジックにする、関数型コンポーネント等)。
ケースバイケースで、必要な対策を取れば大丈夫だと思いますが、その前にupdate
回数が多いところを見つけましょう。
調査はVue.jsのdevtoolsを使います。
ポイントとしては、以下の画像のように値を数箇所変えただけなのにupdated
が数十回〜走っているところは怪しいです。
最後に
プロジェクトの初期から上記のような事項を気にしながら設計することは難しい場合もあると思います。
事後的にこういったパフォーマンスの懸念が出てきた場合でも、まずはコンポーネントの分解から取り掛かりましょう。
AtomicDesignを導入し、機能を複数持った大きなコンポーネントを小さく分解していくことが近道かと思います。
これだけで全て解決とは言えませんが、見通しが良くなることでなにか調子が悪いと感じたときも調査しやすくなるはずです。
以上、長文にお付き合いいただきありがとうございました!