Vueのリアクティブシステムを理解してパフォーマンス低下を防ごう (Roppongi.vue #4 ) ninjinkun.icon
一休.comレストラン フロントエンドエンジニア
これはReact製🙏
https://user-images.githubusercontent.com/113420/50003891-97424b80-ffe8-11e8-949f-def709c14aa5.gif
問題
管理画面をSPAとしてリニューアルの最中、Vueのコンポーネントを多数使った画面で体感速度が遅い問題が発生
https://gyazo.com/7fabf1a8a5758ab68c2a2cbc17f6fbe6
再現アプリ
デモ
1万個のcellそれぞれにチェックボックスが付いている
cellはそれぞれvueコンポーネントでitemオブジェクトを受けとる
https://gyazo.com/9d8cf5545ac7214fb82ba05b34a5f6f3
code:html
// ItemCell.vue
<template>
<div :class="checked: item.checked }">
<div class="name">{{ item.name }}</div>
<input type="checkbox" :value="item.checked" @change="input" />
</div>
</template>
// App.vue
<template>
<div class="row">
<div class="column">
<h2>1. Immutableに全Itemを更新(遅い)</h2>
<table class="table">
<tr v-for="(itemArray, index) in chunk(immutableItems, 10)" :key="index">
<td v-for="item in itemArray" :key="item.key" class="cell">
<ItemCell :item="item" @check="immutableUpdate(item, $event)" />
</td>
</tr>
</table>
</div>
<div class="column">
<h2>2. 直接1アイテムのみを更新(速い)</h2>
<table class="table">
<tr v-for="(itemArray, index) in chunk(mutableItems, 10)" :key="index">
<td v-for="item in itemArray" :key="item.key" class="cell">
<ItemCell :item="item" @check="mutableUpdate(item, $event)" />
</td>
</tr>
</table>
</div>
</div>
</template>
何が問題だったか?
チェックボックスをON/OFFする度に全てのVueコンポーネントが再レンダリングされていた
ChromeのVue Debugで見ると再描画回数がわかる
https://gyazo.com/175c9b64839de1a5b604228cd15b2d7a
なぜ再レンダリングされた?
コンポーネントに渡すオブジェクトを毎回再生成していた
オブジェクトはImmutableに更新する方が良いと考えていた
問題のコード
チェック状態を更新するコード
これだけでもわかる人ははわかると思いますが…
code:js
{
methods: {
immutableUpdate(item, checked) {
this.immutableItems = this.immutableItems.map(i =>
i.id === item.id ? { ...i, checked } : { ...i }
);
}
}
}
根本原因
Vueコンポーネントが再レンダリングされる条件を理解していなかった
Vueのリアクティブシステムを理解する必要がある
Vueのリアクティブシステムとは?
props, dataが更新されると画面が自動更新される
この自動更新を支える仕組みがリアクティブシステム
props, dataに渡した値(プリミティブ、オブジェクト)の更新は監視されている!
ネストされたオブジェクトは、ネスト先まで監視されている!
(少し脱線)リアクティブシステムの実装
code:js
// Object.defineProperty でオブジェクトのプロパティ毎にgetter/setterを注入して監視
obj.keys().forEach(key => {
Object.defineProperty(obj, key, {
get() {
dep.depend() // 使っている箇所を監視
return internalValue
}
set(newValue) {
internalValue = newValue
dep.notify() // 更新を通知
}
})
}
ユーザー側の考え方はそんなに変わらないのではないかと思う
本第
大事なこと: オブジェクトの変更監視はプロパティのgetter/setterベース
ではオブジェクトを丸ごと入れ替えると…?
同一プロパティでも違うオブジェクトと見なされる
そしてコンポーネントの更新が発生!!
これが今回のパフォーマンス低下の正体
code:js
// さっきのコードのどこが問題か?
immutableUpdate(item, checked) {
this.immutableItems = this.immutableItems.map(i =>
i.id === item.id ? { ...i, checked } : { ...i }
);
}
// 間違い1
// オブジェクトを再生成しているので、全componentで再描画が走る(重い)
this.immutableItems.map(i =>
i.id === item.id ? { ...i, checked } : { ...i }
);
// 間違い2
// arrayに再代入しているので、全要素を再度observerに登録する処理が走る(重い)
this.immutableItems = ...
結局どうすればいいの?
変更したオブジェクトをだけをmutableに直接更新しよう
code:js
// 正解 オブジェクトのプロパティだけをmutableに更新
methods: {
mutableUpdate(item, checked) {
item.checked = checked;
}
}
もしかして普通みんなこう書いている…?
なんでこんなコードを書いてしまったか?
ninjinkunはReactの経験からImmutableにオブジェクトを生成してしまっていたため
Reactとの比較
ReactにはVueのようなオブジェクト監視を使ったリアクティブシステムがない
コンポーネントのprops/stateの変更が発生する度、毎回renderメソッドが呼ばれる
これを制御してパフォーマンスをチューニングするためにshouldComponentUpdate React.memoなどが用意されている
自動制御だがmutableなVue、手動制御だがimmutableなReact
まとめ
オブジェクトをImmutableに生成すると再描画が発生して遅くなるので注意!
変更したプロパティをだけをmutableに直接更新しよう
Vueのリアクティブシステムを理解すると捗るぞ
一休ではVueエンジニア、デザイナーを募集しています!
懇親会でお話ししましょう
https://gyazo.com/60fce19a46d2bb825a261ba421e0a962