Cookbook組件形式:優(yōu)化 Vue 組件的運(yùn)行時性能
Vue 2.0 在發(fā)布之初,就以其優(yōu)秀的運(yùn)行時性能著稱,你可以通過這個第三方 benchmark 來對比其他框架的性能。Vue 使用了 Virtual DOM 來進(jìn)行視圖渲染,當(dāng)數(shù)據(jù)變化時,Vue 會對比前后兩棵組件樹,只將必要的更新同步到視圖上。
Vue 幫我們做了很多,但對于一些復(fù)雜場景,特別是大量的數(shù)據(jù)渲染,我們應(yīng)當(dāng)時刻關(guān)注應(yīng)用的運(yùn)行時性能。
本文仿照Vue Cookbook 組織形式,對優(yōu)化 Vue 組件的運(yùn)行時性能進(jìn)行闡述。
基本的示例
在下面的示例中,我們開發(fā)了一個樹形控件,支持基本的樹形結(jié)構(gòu)展示以及節(jié)點(diǎn)的展開與折疊。
我們定義 Tree 組件的接口如下。 data 綁定了樹形控件的數(shù)據(jù),是若干顆樹組成的數(shù)組, children 表示子節(jié)點(diǎn)。 expanded-keys 綁定了展開的節(jié)點(diǎn)的 key 屬性,使用 sync 修飾符來同步組件內(nèi)部觸發(fā)的節(jié)點(diǎn)展開狀態(tài)的更新。
<template>
<tree :data="data" expanded-keys.sync="expandedKeys"></tree>
</template>
<script>
export default {
data() {
return {
data: [{
key: '1',
label: '節(jié)點(diǎn) 1',
children: [{
key: '1-1',
label: '節(jié)點(diǎn) 1-1'
}]
}, {
key: '2',
label: '節(jié)點(diǎn) 2'
}]
}
}
};
</script>
Tree 組件的實(shí)現(xiàn)如下,這是個稍微復(fù)雜的例子,需要花幾分鐘時間閱讀一下。
<template>
<ul class="tree">
<li
v-for="node in nodes"
v-show="status[node.key].visible"
:key="node.key"
class="tree-node"
:style="{ 'padding-left': `${node.level * 16}px` }"
>
<i
v-if="node.children"
class="tree-node-arrow"
:class="{ expanded: status[node.key].expanded }"
@click="changeExpanded(node.key)"
>
</i>
{{ node.label }}
</li>
</ul>
</template>
<script>
export default {
props: {
data: Array,
expandedKeys: {
type: Array,
default: () => [],
},
},
computed: {
// 將 data 轉(zhuǎn)為一維數(shù)組,方便 v-for 進(jìn)行遍歷
// 同時添加 level 和 parent 屬性
nodes() {
return this.getNodes(this.data);
},
// status 是一個 key 和節(jié)點(diǎn)狀態(tài)的一個 Map 數(shù)據(jù)結(jié)構(gòu)
status() {
return this.getStatus(this.nodes);
},
},
methods: {
// 對 data 進(jìn)行遞歸,返回一個所有節(jié)點(diǎn)的一維數(shù)組
getNodes(data, level = 0, parent = null) {
let nodes = [];
data.forEach((item) => {
const node = {
level,
parent,
...item,
};
nodes.push(node);
if (item.children) {
const children = this.getNodes(item.children, level + 1, node);
nodes = [...nodes, ...children];
node.children = children.filter(child => child.level === level + 1);
}
});
return nodes;
},
// 遍歷 nodes,計算每個節(jié)點(diǎn)的狀態(tài)
getStatus(nodes) {
const status = {};
nodes.forEach((node) => {
const parentStatus = status[node.parent && node.parent.key] || {};
status[node.key] = {
expanded: this.expandedKeys.includes(node.key),
visible: node.level === 0 || (parentStatus.expanded && parentStatus.visible),
};
});
return status;
},
// 切換節(jié)點(diǎn)的展開狀態(tài)
changeExpanded(key) {
const index = this.expandedKeys.indexOf(key);
const expandedKeys = [...this.expandedKeys];
if (index >= 0) {
expandedKeys.splice(index, 1);
} else {
expandedKeys.push(key);
}
this.$emit('update:expandedKeys', expandedKeys);
},
},
};
</script>
展開或折疊節(jié)點(diǎn)時,我們只需更新 expanded-keys , status 計算屬性便會自動更新,保證關(guān)聯(lián)子節(jié)點(diǎn)可見狀態(tài)的正確。
一切準(zhǔn)備就緒,為了度量 Tree 組件的運(yùn)行性能,我們設(shè)定了兩個指標(biāo)。
初次渲染時間 節(jié)點(diǎn)展開 / 折疊時間
在 Tree 組件中添加代碼如下,使用 console.time 和 console.timeEnd 可以輸出某個操作的具體耗時。
export default {
// ...
methods: {
// ...
changeExpanded(key) {
// ...
this.$emit('update:expandedKeys', expandedKeys);
console.time('expanded change');
this.$nextTick(() => {
console.timeEnd('expanded change');
});
},
},
beforeCreate() {
console.time('first rendering');
},
mounted() {
console.timeEnd('first rendering');
},
};
同時,為了放大可能存在的性能問題,我們編寫了一個方法來生成可控數(shù)量的節(jié)點(diǎn)數(shù)據(jù)。
<template>
<tree :data="data" :expanded-keys.sync="expandedKeys"></tree>
</template>
<script>
export default {
data() {
return {
// 生成一個有 3 層,每層 10 個共 1000 個節(jié)點(diǎn)的節(jié)點(diǎn)樹
data: this.getRandomData(3, 10),
expandedKeys: [],
};
},
methods: {
getRandomData(layers, count, parent) {
return Array.from({ length: count }, (v, i) => {
const key = (parent ? `${parent.key}-` : '') + (i + 1);
const node = {
key,
label: `節(jié)點(diǎn) ${key}`,
};
if (layers > 1) {
node.children = this.getRandomData(layers - 1, count, node);
}
return node;
});
},
},
};
<script>
你可以通過這個CodeSandbox 完整示例來實(shí)際觀察下性能損耗。點(diǎn)擊箭頭展開或折疊某個節(jié)點(diǎn),在 Chrome DevTools 的控制臺(不要使用 CodeSandbox 的控制臺,不準(zhǔn)確)中輸出如下。
first rendering: 406.068115234375ms expanded change: 231.623779296875ms
在筆者的低功耗筆記本下,初次渲染耗時 400+ms,展開或折疊節(jié)點(diǎn) 200+ms。下面我們來優(yōu)化 Tree 組件的運(yùn)行性能。
若你的設(shè)備性能強(qiáng)勁,可修改生成的節(jié)點(diǎn)數(shù)量,如 this.getRandomData(4, 10) 生成 10000 個節(jié)點(diǎn)。
使用 Chrome Performance 查找性能瓶頸
Chrome 的 Performance 面板可以錄制一段時間內(nèi)的 js 執(zhí)行細(xì)節(jié)及時間。使用 Chrome 開發(fā)者工具分析頁面性能的步驟如下。
打開 Chrome 開發(fā)者工具,切換到 Performance 面板 點(diǎn)擊 Record 開始錄制 刷新頁面或展開某個節(jié)點(diǎn) 點(diǎn)擊 Stop 停止錄制 
console.time 輸出的值也會顯示在 Performance 中,幫助我們調(diào)試。更多關(guān)于 Performance 的內(nèi)容可以點(diǎn)擊這里查看。
優(yōu)化運(yùn)行時性能
條件渲染
我們往下翻閱 Performance 分析結(jié)果,發(fā)現(xiàn)大部分耗時都在 render 函數(shù)上,并且下面還有很多其他函數(shù)的調(diào)用。

在遍歷節(jié)點(diǎn)時,對于節(jié)點(diǎn)的可見性我們使用的是 v-show 指令,不可見的節(jié)點(diǎn)也會渲染出來,然后通過樣式使其不可見。因此嘗試使用 v-if 指令來進(jìn)行條件渲染。
<li
v-for="node in nodes"
v-if="status[node.key].visible"
:key="node.key"
class="tree-node"
:style="{ 'padding-left': `${node.level * 16}px` }"
>
...
</li>
v-if 在 render 函數(shù)中表現(xiàn)為一個三目表達(dá)式:
visible ? h('li') : this._e() // this._e() 生成一個注釋節(jié)點(diǎn)
即 v-if 只是減少每次遍歷的時間,并不能減少遍歷的次數(shù)。且Vue.js 風(fēng)格指南中明確指出不要把 v-if 和 v-for 同時用在同一個元素上,因?yàn)檫@可能會導(dǎo)致不必要的渲染。
我們可以更換為在一個可見節(jié)點(diǎn)的計算屬性上進(jìn)行遍歷:
<li
v-for="node in visibleNodes"
:key="node.key"
class="tree-node"
:style="{ 'padding-left': `${node.level * 16}px` }"
>
...
</li>
<script>
export {
// ...
computed: {
visibleNodes() {
return this.nodes.filter(node => this.status[node.key].visible);
},
},
// ...
}
</script>
優(yōu)化后的性能耗時如下:
first rendering: 194.7890625ms expanded change: 204.01904296875ms
你可以通過改進(jìn)后的示例 (Demo2) 來觀察組件的性能損耗,相比優(yōu)化前有很大的提升。
雙向綁定
在前面的示例中,我們使用 .sync 對 expanded-keys 進(jìn)行了“雙向綁定”,其實(shí)際上是 prop 和自定義事件的語法糖。這種方式能很方便地讓 Tree 的父組件同步展開狀態(tài)的更新。
但是,使用 Tree 組件時,不傳 expanded-keys ,會導(dǎo)致節(jié)點(diǎn)無法展開或折疊,即使你不關(guān)心展開或折疊的操作。這里把 expanded-keys 作為外界的副作用了。
<!-- 無法展開 / 折疊節(jié)點(diǎn) --> <tree :data="data"></tree>
這里還存在一些性能問題,展開或折疊某一節(jié)點(diǎn)時,觸發(fā)父組件的副作用更新 expanded-keys 。Tree 組件的 status 依賴了 expanded-keys ,會調(diào)用 this.getStatus 方法獲取新的 status 。即使只是單個節(jié)點(diǎn)的狀態(tài)改變,也會導(dǎo)致重新計算所有節(jié)點(diǎn)的狀態(tài)。
我們考慮將 status 作為一個 Tree 組件的內(nèi)部狀態(tài),展開或折疊某個節(jié)點(diǎn)時,直接對 status 進(jìn)行修改。同時定義默認(rèn)的展開節(jié)點(diǎn) default-expanded-keys 。 status 只在初始化時依賴 default-expanded-keys 。
export default {
props: {
data: Array,
// 默認(rèn)展開節(jié)點(diǎn)
defaultExpandedKeys: {
type: Array,
default: () => [],
},
},
data() {
return {
status: null, // status 為局部狀態(tài)
};
},
computed: {
nodes() {
return this.getNodes(this.data);
},
},
watch: {
nodes: {
// nodes 改變時重新計算 status
handler() {
this.status = this.getStatus(this.nodes);
},
// 初始化 status
immediate: true,
},
// defaultExpandedKeys 改變時重新計算 status
defaultExpandedKeys() {
this.status = this.getStatus(this.nodes);
},
},
methods: {
getNodes(data, level = 0, parent = null) {
// ...
},
getStatus(nodes) {
// ...
},
// 展開或折疊節(jié)點(diǎn)時直接修改 status,并通知父組件
changeExpanded(key) {
console.time('expanded change');
const node = this.nodes.find(n => n.key === key); // 找到該節(jié)點(diǎn)
const newExpanded = !this.status[key].expanded; // 新的展開狀態(tài)
// 遞歸該節(jié)點(diǎn)的后代節(jié)點(diǎn),更新 status
const updateVisible = (n, visible) => {
n.children.forEach((child) => {
this.status[child.key].visible = visible && this.status[n.key].expanded;
if (child.children) updateVisible(child, visible);
});
};
this.status[key].expanded = newExpanded;
updateVisible(node, newExpanded);
// 觸發(fā)節(jié)點(diǎn)展開狀態(tài)改變事件
this.$emit('expanded-change', node, newExpanded, this.nodes.filter(n => this.status[n.key].expanded));
this.$nextTick(() => {
console.timeEnd('expanded change');
});
},
},
beforeCreate() {
console.time('first rendering');
},
mounted() {
console.timeEnd('first rendering');
},
};
使用 Tree 組件時,即使不傳 default-expanded-keys ,節(jié)點(diǎn)也能正常地展開或收起。
<!-- 節(jié)點(diǎn)可以展開或收起 --> <tree :data="data"></tree> <!-- 配置默認(rèn)展開的節(jié)點(diǎn) --> <tree :data="data" :default-expanded-keys="['1', '1-1']" @expanded-change="handleExpandedChange" > </tree>
優(yōu)化后的性能耗時如下。
first rendering: 91.48193359375ms expanded change: 20.4287109375ms
你可以通過改進(jìn)后的示例 (Demo3) 來觀察組件的性能損耗。
凍結(jié)數(shù)據(jù)
到此為止,Tree 組件的性能問題已經(jīng)不是很明顯了。為了進(jìn)一步擴(kuò)大性能問題,查找優(yōu)化空間。我們把節(jié)點(diǎn)數(shù)量增加到 10000 個。
// 生成 10000 個節(jié)點(diǎn) this.getRandomData(4, 1000)
這里,我們故意制造一個可能存在性能問題的改動。雖然這不是必須的,當(dāng)它能幫助我們了解接下來所要介紹的問題。
將計算屬性 nodes 修改為在 data 的 watcher 中去獲取 nodes 的值。
export default {
// ...
watch: {
data: {
handler() {
this.nodes = this.getNodes(this.data);
this.status = this.getStatus(this.nodes);
},
immediate: true,
},
// ...
},
// ...
};
這種修改對于實(shí)現(xiàn)的功能是沒有影響的,那么性能情況如何呢。
first rendering: 490.119140625ms expanded change: 183.94189453125ms
使用 Performance 工具嘗試查找性能瓶頸。

我們發(fā)現(xiàn),在 getNodes 方法調(diào)用之后,有一段耗時很長的 proxySetter 。這是 Vue 在為 nodes 屬性添加響應(yīng)式,讓 Vue 能夠追蹤依賴的變化。 getStatus 同理。
當(dāng)你把一個普通的 JavaScript 對象傳給 Vue 實(shí)例的 data 選項(xiàng),Vue 將遍歷此對象所有的屬性,并使用 Object.defineProperty 把這些屬性全部轉(zhuǎn)為 getter/setter。
對象越復(fù)雜,層級越深,這個過程消耗的時間越長。當(dāng)我們存在 1w 個節(jié)點(diǎn)時, proxySetter 的時間就會非常長了。
這里存在一個問題,我們不會對 nodes 某個具體的屬性做修改,而是每當(dāng) data 變化時重新去計算一次。因此,這里為 nodes 添加的響應(yīng)式是無用的。那么怎么把不需要的 proxySetter 去掉呢?一種方法是將 nodes 改回計算屬性,一般情況下計算屬性沒有賦值行為。另一種方法就是凍結(jié)數(shù)據(jù)。
使用 Object.freeze() 來凍結(jié)數(shù)據(jù),這會阻止修改現(xiàn)有的屬性,也意味著響應(yīng)系統(tǒng)無法再追蹤變化。
this.nodes = Object.freeze(this.getNodes(this.data));
查看 Performance 工具, getNodes 方法后已經(jīng)沒有 proxySetter 了。

性能指標(biāo)如下,對于初次渲染的提升還是很可觀的。
first rendering: 312.22998046875ms expanded change: 179.59326171875ms
你可以通過改進(jìn)后的示例 (Demo4) 來觀察組件的性能損耗。
那我們能否用同樣的辦法優(yōu)化 status 的跟蹤呢?答案是否定的,因?yàn)槲覀冃枰ジ?status 中的屬性值 ( changeExpanded )。因此,這種優(yōu)化只適用于其屬性不會被更新,只會更新整個對象的數(shù)據(jù)。且對于結(jié)構(gòu)越復(fù)雜、層級越深的數(shù)據(jù),優(yōu)化效果越明顯。
替代方案
我們看到,示例中不管是節(jié)點(diǎn)的渲染還是數(shù)據(jù)的計算,都存在大量的循環(huán)或遞歸。對于這種大量數(shù)據(jù)的問題,除了上述提到的針對 Vue 的優(yōu)化外,我們還可以從減少每次循環(huán)的耗時和減少循環(huán)次數(shù)兩個方面進(jìn)行優(yōu)化。
例如,可以使用字典來優(yōu)化數(shù)據(jù)查找。
// 生成 defaultExpandedKeys 的 Map 對象
const expandedKeysMap = this.defaultExpandedKeys.reduce((map, key) => {
map[key] = true;
return map;
}, {});
// 查找時
if (expandedKeysMap[key]) {
// do something
}
defaultExpandedKeys.includes 的事件復(fù)雜度是 O(n), expandedKeysMap[key] 的時間復(fù)雜度是 O(1)。
更多關(guān)于優(yōu)化 Vue 應(yīng)用性能可以查看Vue 應(yīng)用性能優(yōu)化指南。
這樣做的價值
應(yīng)用性能對于用戶體驗(yàn)的提升是非常重要的,也往往是容易被忽視的。試想一下,一個在某臺設(shè)備運(yùn)行良好的應(yīng)用,到了另一臺配置較差的設(shè)備上導(dǎo)致用戶瀏覽器崩潰了,這一定是一個不好的體驗(yàn)。又或者你的應(yīng)用在常規(guī)數(shù)據(jù)下正常運(yùn)行,卻在大數(shù)據(jù)量下需要相當(dāng)長的等待時間,也許你就因此錯失了一部分用戶。
總結(jié)
性能優(yōu)化是一個長久不衰的話題,沒有一種通用的辦法能夠解決所有的性能問題。性能優(yōu)化是可以持續(xù)不端地進(jìn)行下去的,但隨著問題的深入,性能瓶頸會越來越不明顯,優(yōu)化也越困難。
本文的示例具有一定的特殊性,但它為我們指引了性能優(yōu)化的方法論。
- 確定衡量運(yùn)行時性能的指標(biāo)
- 確定優(yōu)化目標(biāo),例如實(shí)現(xiàn) 1W+ 數(shù)據(jù)的秒出
- 使用工具(Chrome Performance)分析性能問題
- 優(yōu)先解決問題的大頭(瓶頸)
- 重復(fù) 3 4 步直到實(shí)現(xiàn)目標(biāo)
以上所述是小編給大家介紹的Cookbook組件形式:優(yōu)化 Vue 組件的運(yùn)行時性能,希望對大家有所幫助,如果大家有任何疑問歡迎給我留言,小編會及時回復(fù)大家的!
相關(guān)文章
vue基礎(chǔ)之事件v-onclick="函數(shù)"用法示例
這篇文章主要介紹了vue基礎(chǔ)之事件v-onclick="函數(shù)"用法,結(jié)合實(shí)例形式分析了vue.js事件v-on:click="函數(shù)"的data數(shù)據(jù)添加、點(diǎn)擊響應(yīng)、以及留言本功能相關(guān)操作技巧,需要的朋友可以參考下2019-03-03
如何優(yōu)雅地在vue中添加權(quán)限控制示例詳解
這篇文章主要給大家介紹了關(guān)于如何優(yōu)雅地在vue中添加權(quán)限控制的相關(guān)資料,文中通過示例代碼以及圖文介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03
使用antv替代Echarts實(shí)現(xiàn)數(shù)據(jù)可視化圖表詳解
這篇文章主要為大家介紹了使用antv替代Echarts實(shí)現(xiàn)數(shù)據(jù)可視化圖表詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
vue項(xiàng)目百度地圖如何自定義標(biāo)注marker
這篇文章主要介紹了vue項(xiàng)目百度地圖如何自定義標(biāo)注marker問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-03-03
使用element-ui,el-row中的el-col數(shù)據(jù)為空頁面布局變亂問題
這篇文章主要介紹了使用element-ui,el-row中的el-col數(shù)據(jù)為空頁面布局變亂問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-08-08

