今天要來談談 Vue官網效能優化 的部分,昨天我們複習了使用 JavaScript 動態載入
的特性來進行程式碼分割,這種方法將程式碼拆分成不同檔案,減少了初次渲染時請求的 JS 檔案過於肥大,進而提升加載速度,這是一種常見的優化技巧。不過,在理解了元件間的資料傳遞行為後,我們可能會想知道,Vue 的 Virtual DOM 更新機制是否可以精確地更新有資料變更的節點,而不像 React 常提到的重新渲染問題(re-render)
? 今天花點時間來探討這個議題吧~
a child component only updates when at least one of its received props has changed.
字面上看到「保持 props 的穩定度」一開始會不太容易直觀理解,意思是讓傳入的 props 儘量維持穩定、不頻繁變化,以避免元件內部的額外計算和不必要的重新渲染,因為一個元件可能會接收多個 props,只要其中一個有變動,子元件就會重新渲染
。
實際看一下範例會更好懂:
我們會發現應該只針對 activeId等於子元件id
時,前面加入>符號重新渲染,這邊我們用生命週期 onUpdated
來捕捉實際元件的更新,但實際上按下按鈕是整個列表的元件都重新渲染了。
// 可以改成 isActive 布林傳入就不會導致重新渲染
<template>
<ul>
<ListItem
v-for="item in messages"
:key="item.id"
:id="item.id"
:message="item.message"
:activeId="activeId" // 移除activeId
:isActive= activeId === id // 這裡修改
/>
</ul>
<button @click="next">Next</button>
</template>
如果有兩個或多個 props 需要在子組件內進行比較或計算,建議在父層使用 computed 計算好結果,或是直接在樣板上以JS表達式計算完再傳入
。而不是讓子元件接收props處理。這樣可以減少子元件的複雜度和依賴,使得元更具穩定性,不用因為其中依賴一個props變動導致整個元件重新渲染。
<template>
<div>
<span v-show="activeId === id">></span>
<span> {{ message }}</span>
</div>
</template>
// 可以改為
<template>
<div>
<span v-show="isActive">></span>
<span> {{ message }}</span>
</div>
</template>
如果有接觸過 React 多少會有耳聞重新渲染(re-render)問題,通常指的是父元件往子元件傳遞多個props資料時,但某些子元件本身接收到的props沒有變動也會重新渲染
,那麼 Vue 本身是否也有這個問題?,在此之前先來看一下 Virtual DOM
的更新結構:
因為 Virtual DOM
本身是一個樹狀結構圖,當有一個中間的父節點接收到的資料更新時,會從該節點往下一起更新,是一種 DOM 模板樹結構上的依賴。即使子元件的props依賴項並未改變,父元件必須重新渲染,而影響到內嵌的子元件。
(圖片出處)
在早期2016年Vue論壇中有人談論此問題,而尤雨溪本人也有做出回應:
大意是: 因為早期渲染函式會在父元件的執行環境中直接執行,子元件本身沒有自己的實例狀態。由於沒有實例與函數式元件相關聯,所以很難針對先前的渲染結果進行記憶(memoization)來達到優化效果,不容易實現控制子元件在某些特定條件下不重新渲染。
先前文章,Day 2: Vue SFC樣板(Template)和渲染函式(Render Function),有提到每個Vue3元件檔其實最終都會變成一段渲染函式,渲染函式透過 <script setup>
利用函式閉包特性將資料整個封閉起來,每個元件也都有自己資料實例狀態和一些獨立生命週期,慢慢突破在元件間記憶(memoization)上的實現
。
後來Vue3.2推出改版後,出一個新的指令v-memo,讓開發者自行決定元件依賴的那些props改變時,需要重新渲染來達到優化效果,主要是為了避免大型表v-for渲染時,因為大型Virtual DOM樹狀結構的重繪效能的損失
。
不過這種情況來說一般比較屬於特例,雖然可以避免上面Props Stable的情況發生,一般子元件DOM結構單位很小,耗損記憶體和重新執行的效率可能不成正比,就避免過度濫用
。
<div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
<p>ID: {{ item.id }} - selected: {{ item.id === selected }}</p>
<p>...more child nodes</p>
</div>
其實一個元件接收到任何外部props資料有異動時,雖然都會立刻觸發render function
,導致產生新的Virtual DOM來進行比較(重新渲染)
,不過綁定到樣板上的資料其實後續還會進行比較,真的映射到樣板上資料有更新時,才會調動Vue runtime-dom模組
去對真實瀏覽器做更新動作,並不會Virtual DOM有重繪的部分一律更新到瀏覽器上。 (註: 因為 Virtual DOM 比較的成本相對較低,可以先計算出差異再執行最小範圍的更新,從而減少不必要的 DOM 操作。
)
computed 穩定度
的環節主要是針對用Vue監聽器進行computed的監聽的問題,像下面範例因為 computed
回傳是一般型態資料型別-布林值,可以很直觀地認為當 true/fasle 有變化時,才觸發對應的 watchEffect回調
。
但如果computed返回是物件,因為物件是全新的引用,有可能造成不必要資料更新
const count = ref(0)
const isEven = computed(() => count.value % 2 === 0)
watchEffect(() => console.log(isEven.value)) // true
// will not trigger new logs because the computed value stays `true`
count.value = 2
count.value = 4
// 但如果computed返回是物件,因為物件是全新的引用,有可能造成不必要資料更新
const computedObj = computed(() => {
return {
isEven: count.value % 2 === 0
}
})
利用 computed
接收的參數 oldValue
,當內部屬性新值與舊值相等時,確保同一個物件的引用資料保持不變。這樣一來,Vue 可以確定實際內部所依賴的資料,來判斷是否需要進一步處理,進而避免因用物件引用位置不同所造成的更新渲染操作。
const computedObj = computed((oldValue) => {
const newValue = {
isEven: count.value % 2 === 0
}
if (oldValue && oldValue.isEven === newValue.isEven) {
return oldValue
}
return newValue
})
Vue其實也有重新渲染問題(re-render),但今天學到了如何減少 Vue 的重新渲染:
保持 props 穩定性:
在父元件處理好必要的計算和判斷後,再傳入子元件。這樣可以減少子元件內部的複雜性,也能降低重新渲染的頻率。
合理使用 v-memo,不過度濫用:
對於大型列表或子元件用有非常大型DOM結構(sub tree)並且影響效能時
,可以使用 v-memo
指令以控制重新渲染的條件。
合理使用 computed 和 watch:避免 computed 屬性中每次都返回新的物件或陣列
。如果要使用,可以使用 參數 oldValue
來比較新舊值,並僅在必要時返回新物件,這樣能減少重新計算和重新渲染。