Cookbook組件形式:優(yōu)化 Vue 組件的運行時性能
Vue 2.0 在發(fā)布之初,就以其優(yōu)秀的運行時性能著稱,你可以通過這個第三方 benchmark 來對比其他框架的性能。Vue 使用了 Virtual DOM 來進行視圖渲染,當(dāng)數(shù)據(jù)變化時,Vue 會對比前后兩棵組件樹,只將必要的更新同步到視圖上。
Vue 幫我們做了很多,但對于一些復(fù)雜場景,特別是大量的數(shù)據(jù)渲染,我們應(yīng)當(dāng)時刻關(guān)注應(yīng)用的運行時性能。
本文仿照Vue Cookbook 組織形式,對優(yōu)化 Vue 組件的運行時性能進行闡述。
基本的示例
在下面的示例中,我們開發(fā)了一個樹形控件,支持基本的樹形結(jié)構(gòu)展示以及節(jié)點的展開與折疊。
我們定義 Tree 組件的接口如下。 data
綁定了樹形控件的數(shù)據(jù),是若干顆樹組成的數(shù)組, children
表示子節(jié)點。 expanded-keys
綁定了展開的節(jié)點的 key
屬性,使用 sync
修飾符來同步組件內(nèi)部觸發(fā)的節(jié)點展開狀態(tài)的更新。
<template> <tree :data="data" expanded-keys.sync="expandedKeys"></tree> </template> <script> export default { data() { return { data: [{ key: '1', label: '節(jié)點 1', children: [{ key: '1-1', label: '節(jié)點 1-1' }] }, { key: '2', label: '節(jié)點 2' }] } } }; </script>
Tree 組件的實現(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 進行遍歷 // 同時添加 level 和 parent 屬性 nodes() { return this.getNodes(this.data); }, // status 是一個 key 和節(jié)點狀態(tài)的一個 Map 數(shù)據(jù)結(jié)構(gòu) status() { return this.getStatus(this.nodes); }, }, methods: { // 對 data 進行遞歸,返回一個所有節(jié)點的一維數(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é)點的狀態(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é)點的展開狀態(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é)點時,我們只需更新 expanded-keys
, status
計算屬性便會自動更新,保證關(guān)聯(lián)子節(jié)點可見狀態(tài)的正確。
一切準(zhǔn)備就緒,為了度量 Tree 組件的運行性能,我們設(shè)定了兩個指標(biāo)。
初次渲染時間 節(jié)點展開 / 折疊時間
在 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é)點數(shù)據(jù)。
<template> <tree :data="data" :expanded-keys.sync="expandedKeys"></tree> </template> <script> export default { data() { return { // 生成一個有 3 層,每層 10 個共 1000 個節(jié)點的節(jié)點樹 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é)點 ${key}`, }; if (layers > 1) { node.children = this.getRandomData(layers - 1, count, node); } return node; }); }, }, }; <script>
你可以通過這個CodeSandbox 完整示例來實際觀察下性能損耗。點擊箭頭展開或折疊某個節(jié)點,在 Chrome DevTools 的控制臺(不要使用 CodeSandbox 的控制臺,不準(zhǔn)確)中輸出如下。
first rendering: 406.068115234375ms expanded change: 231.623779296875ms
在筆者的低功耗筆記本下,初次渲染耗時 400+ms,展開或折疊節(jié)點 200+ms。下面我們來優(yōu)化 Tree 組件的運行性能。
若你的設(shè)備性能強勁,可修改生成的節(jié)點數(shù)量,如 this.getRandomData(4, 10)
生成 10000 個節(jié)點。
使用 Chrome Performance 查找性能瓶頸
Chrome 的 Performance 面板可以錄制一段時間內(nèi)的 js 執(zhí)行細節(jié)及時間。使用 Chrome 開發(fā)者工具分析頁面性能的步驟如下。
打開 Chrome 開發(fā)者工具,切換到 Performance 面板 點擊 Record 開始錄制 刷新頁面或展開某個節(jié)點 點擊 Stop 停止錄制
console.time
輸出的值也會顯示在 Performance 中,幫助我們調(diào)試。更多關(guān)于 Performance 的內(nèi)容可以點擊這里查看。
優(yōu)化運行時性能
條件渲染
我們往下翻閱 Performance 分析結(jié)果,發(fā)現(xiàn)大部分耗時都在 render 函數(shù)上,并且下面還有很多其他函數(shù)的調(diào)用。
在遍歷節(jié)點時,對于節(jié)點的可見性我們使用的是 v-show
指令,不可見的節(jié)點也會渲染出來,然后通過樣式使其不可見。因此嘗試使用 v-if
指令來進行條件渲染。
<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)為一個三目表達式:
visible ? h('li') : this._e() // this._e() 生成一個注釋節(jié)點
即 v-if
只是減少每次遍歷的時間,并不能減少遍歷的次數(shù)。且Vue.js 風(fēng)格指南中明確指出不要把 v-if
和 v-for
同時用在同一個元素上,因為這可能會導(dǎo)致不必要的渲染。
我們可以更換為在一個可見節(jié)點的計算屬性上進行遍歷:
<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
你可以通過改進后的示例 (Demo2) 來觀察組件的性能損耗,相比優(yōu)化前有很大的提升。
雙向綁定
在前面的示例中,我們使用 .sync
對 expanded-keys
進行了“雙向綁定”,其實際上是 prop 和自定義事件的語法糖。這種方式能很方便地讓 Tree 的父組件同步展開狀態(tài)的更新。
但是,使用 Tree 組件時,不傳 expanded-keys
,會導(dǎo)致節(jié)點無法展開或折疊,即使你不關(guān)心展開或折疊的操作。這里把 expanded-keys
作為外界的副作用了。
<!-- 無法展開 / 折疊節(jié)點 --> <tree :data="data"></tree>
這里還存在一些性能問題,展開或折疊某一節(jié)點時,觸發(fā)父組件的副作用更新 expanded-keys
。Tree 組件的 status
依賴了 expanded-keys
,會調(diào)用 this.getStatus
方法獲取新的 status
。即使只是單個節(jié)點的狀態(tài)改變,也會導(dǎo)致重新計算所有節(jié)點的狀態(tài)。
我們考慮將 status
作為一個 Tree 組件的內(nèi)部狀態(tài),展開或折疊某個節(jié)點時,直接對 status
進行修改。同時定義默認的展開節(jié)點 default-expanded-keys
。 status
只在初始化時依賴 default-expanded-keys
。
export default { props: { data: Array, // 默認展開節(jié)點 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é)點時直接修改 status,并通知父組件 changeExpanded(key) { console.time('expanded change'); const node = this.nodes.find(n => n.key === key); // 找到該節(jié)點 const newExpanded = !this.status[key].expanded; // 新的展開狀態(tài) // 遞歸該節(jié)點的后代節(jié)點,更新 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é)點展開狀態(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é)點也能正常地展開或收起。
<!-- 節(jié)點可以展開或收起 --> <tree :data="data"></tree> <!-- 配置默認展開的節(jié)點 --> <tree :data="data" :default-expanded-keys="['1', '1-1']" @expanded-change="handleExpandedChange" > </tree>
優(yōu)化后的性能耗時如下。
first rendering: 91.48193359375ms expanded change: 20.4287109375ms
你可以通過改進后的示例 (Demo3) 來觀察組件的性能損耗。
凍結(jié)數(shù)據(jù)
到此為止,Tree 組件的性能問題已經(jīng)不是很明顯了。為了進一步擴大性能問題,查找優(yōu)化空間。我們把節(jié)點數(shù)量增加到 10000 個。
// 生成 10000 個節(jié)點 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, }, // ... }, // ... };
這種修改對于實現(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 實例的 data
選項,Vue 將遍歷此對象所有的屬性,并使用 Object.defineProperty 把這些屬性全部轉(zhuǎn)為 getter/setter。
對象越復(fù)雜,層級越深,這個過程消耗的時間越長。當(dāng)我們存在 1w 個節(jié)點時, 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
你可以通過改進后的示例 (Demo4) 來觀察組件的性能損耗。
那我們能否用同樣的辦法優(yōu)化 status
的跟蹤呢?答案是否定的,因為我們需要去更新 status
中的屬性值 ( changeExpanded
)。因此,這種優(yōu)化只適用于其屬性不會被更新,只會更新整個對象的數(shù)據(jù)。且對于結(jié)構(gòu)越復(fù)雜、層級越深的數(shù)據(jù),優(yōu)化效果越明顯。
替代方案
我們看到,示例中不管是節(jié)點的渲染還是數(shù)據(jù)的計算,都存在大量的循環(huán)或遞歸。對于這種大量數(shù)據(jù)的問題,除了上述提到的針對 Vue 的優(yōu)化外,我們還可以從減少每次循環(huán)的耗時和減少循環(huán)次數(shù)兩個方面進行優(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)用性能對于用戶體驗的提升是非常重要的,也往往是容易被忽視的。試想一下,一個在某臺設(shè)備運行良好的應(yīng)用,到了另一臺配置較差的設(shè)備上導(dǎo)致用戶瀏覽器崩潰了,這一定是一個不好的體驗。又或者你的應(yīng)用在常規(guī)數(shù)據(jù)下正常運行,卻在大數(shù)據(jù)量下需要相當(dāng)長的等待時間,也許你就因此錯失了一部分用戶。
總結(jié)
性能優(yōu)化是一個長久不衰的話題,沒有一種通用的辦法能夠解決所有的性能問題。性能優(yōu)化是可以持續(xù)不端地進行下去的,但隨著問題的深入,性能瓶頸會越來越不明顯,優(yōu)化也越困難。
本文的示例具有一定的特殊性,但它為我們指引了性能優(yōu)化的方法論。
- 確定衡量運行時性能的指標(biāo)
- 確定優(yōu)化目標(biāo),例如實現(xiàn) 1W+ 數(shù)據(jù)的秒出
- 使用工具(Chrome Performance)分析性能問題
- 優(yōu)先解決問題的大頭(瓶頸)
- 重復(fù) 3 4 步直到實現(xiàn)目標(biāo)
以上所述是小編給大家介紹的Cookbook組件形式:優(yōu)化 Vue 組件的運行時性能,希望對大家有所幫助,如果大家有任何疑問歡迎給我留言,小編會及時回復(fù)大家的!
相關(guān)文章
手寫Vue源碼之?dāng)?shù)據(jù)劫持示例詳解
這篇文章主要給大家介紹了手寫Vue源碼之?dāng)?shù)據(jù)劫持的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01vue 使用插槽分發(fā)內(nèi)容操作示例【單個插槽、具名插槽、作用域插槽】
這篇文章主要介紹了vue 使用插槽分發(fā)內(nèi)容操作,結(jié)合實例形式總結(jié)分析了vue.js使用單個插槽、具名插槽、作用域插槽相關(guān)操作技巧與注意事項,需要的朋友可以參考下2020-03-03vue前端el-input輸入限制輸入位數(shù)及輸入規(guī)則
這篇文章主要給大家介紹了關(guān)于vue前端el-input輸入限制輸入位數(shù)及輸入規(guī)則的相關(guān)資料,文中通過代碼介紹的介紹的非常詳細,對大家學(xué)習(xí)或者使用vue具有一定的參考借鑒價值,需要的朋友可以參考下2023-09-09