演示代碼使用 Vue3 + ts + Vite 撰寫,但是也會列出適用于 Vue2 的優化技巧,如果某個優化只適用于 Vue3 或者 Vue2,我會在標題中標出來,
代碼優化
v-for 中使用 key
使用 v-for 更新已經渲染的元素串列時,默認用就地復用策略;串列資料修改的時候,他會根據 key 值去判斷某個值是否修改,如果修改,則重新渲染這一項,否則復用之前的元素;
使用key的注意事項:
- 不要使用可能重復的或者可能變化
key值(控制臺也會給出提醒) - 如果陣列中的資料有狀態需要維持時(例如輸入框),不要使用陣列的
index作為key值,因為如果在陣列中插入或者移除一個元素時,其后面的元素 index 將會變化,這回讓vue進行原地復用時錯誤的系結狀態, - 如果陣列中沒有唯一的 key值可用,且陣列更新時不是全量更新而是采用類似push,splice來插入或者移除資料時,可以考慮對其添加一個 key 欄位,值為 Symbol() 即可保證唯一,
何時使用何種key?
這是一個非常有考究的問題,首先你要知道 vue 中的 原地復用 (大概就是 虛擬dom 變化時,兩個 虛擬dom節點 的 key 如果一樣就不會重新創建節點,而是修改原來的節點),
當我們渲染的資料不需要保持狀態時,例如常見的單純的表格分頁渲染(不包含輸入,只是展示)、下拉加載更多等場景,那么使用 index 作為 key 再好不過,因為進入下一頁或者上一頁時就會原地復用之前的節點,而不是重新創建,如果使用唯一的 id 作為 key 反而會重新創建dom,性能相對較低,
此外使用 index 作為 key 我還應該要盡量避免對陣列的中間進行 增加/洗掉 等會影響后面元素key變化的操作,這會讓 vue 認為后面所有元素都發生了變化,導致多余的對比和原地復用,
所以使用 index 作為 key 需要滿足:
- 資料沒有獨立的狀態
- 資料不會進行 增加/洗掉 等會影響后面元素key變化的操作
哪何時使用 id 作為 key 呢?
對于大多數資料的 id 都是唯一的,這無疑的一個 key 的優選答案,對于任何大多數情況使用 id 作為 key 都不會出現上面 bug,但是如果你需要考慮性能問題,那就就要思考是否應該使用原地復用了,
同樣是上面的分頁資料展示,如果使用 id 作為 key ,可想而知每一頁的每一條資料 id 都是不一樣的,所以當換頁時兩顆 虛擬DOM樹 的節點的 key 完全不一致,vue 就會移除原來的節點然后創建新的節點,可想而知效率會更加低下,但是他也有它的優點,唯一的 key 可以幫助 diff 更加精確的為我們系結狀態,這尤其適合資料有獨立的狀態的場景,例如帶輸入框或者單選框的串列資料,
所以何時使用 id 作為 key?只有一點:
- 無法使用 index 作為 key 的時候
v-for 和 v-if 不要一起使用(Vue2)
此優化技巧僅限于Vue2,Vue3 中對 v-for 和 v-if 的優先級做了調整
這個大家都知道
永遠不要把
v-if和v-for同時用在同一個元素上,
原因是 v-for 的 優先級高于 v-if,所以當它們使用再同一個標簽上是,每一個渲染都會先回圈再進行條件判斷,
注意: Vue3 中
v-if優先級高于v-for,所以當v-for和v-if一起使用時效果類似于Vue2中把v-if上提的效果
例如下面這段代碼在 Vue2 中是不被推薦的,Vue 也會給出對應的警告.
<ul>
<li v-for="user in users" v-if="user.active">
{{ user.name }}
</li>
</ul>
我們應該盡量將 v-if 移動到上級或者使用計算屬性來處理資料.
<ul v-if="active">
<li v-for="user in users">
{{ user.name }}
</li>
</ul>
如果你不想讓回圈的內容多出一個無需有的上級容器,那么你可以選擇使用 template 來作為其父元素,template 不會被瀏覽器渲染為 DOM 節點.
如果我想要判斷遍歷物件里面每一項的內容來選擇渲染的資料的話,可以使用 computed 來對遍歷物件進行過濾.
// js
let usersActive = computed(()=>users.filter(user => user.active))
// template
<ul>
<li v-for="user in usersActive">
{{ user.name }}
</li>
</ul>
合理的選擇 v-if 和 v-show
v-if 和 v-show 的區別相比大家都非常熟悉了;v-if 通過直接操作 DOM 的洗掉和添加來控制元素的顯示和隱藏;v-show 是通過控制 DOM 的 display CSS熟悉來控制元素的顯示和隱藏.
由于對 DOM 的 添加/洗掉 操作性能遠遠低于操作 DOM 的 CSS 屬性.
所以當元素需要頻繁的 顯示/隱藏 變化時,我們使用 v-show 來提高性能,
當元素不需要頻繁的 顯示/隱藏 變化時,我們通過 v-if 來移除 DOM 可以節約掉瀏覽器渲染這個的一部分DOM需要的資源.
使用簡單的計算屬性
應該把復雜計算屬性分割為盡可能多的更簡單的 property,
- 易于測驗
當每個計算屬性都包含一個非常簡單且很少依賴的運算式時,撰寫測驗以確保其正確作業就會更加容易,- 易于閱讀
簡化計算屬性要求你為每一個值都起一個描述性的名稱,即便它不可復用,這使得其他開發者 (以及未來的你) 更容易專注在他們關心的代碼上并搞清楚發生了什么,- 更好的“擁抱變化”
任何能夠命名的值都可能用在視圖上,舉個例子,我們可能打算展示一個資訊,告訴用戶他們存了多少錢;也可能打算計算稅費,但是可能會分開展現,而不是作為總價的一部分,
小的、專注的計算屬性減少了資訊使用時的假設性限制,所以需求變更時也用不著那么多重構了,
computed 大家很熟悉, 它會在其運算式中依賴的回應式資料發送變化時重新計算,如果我們在一個計算屬性中書寫了比較復雜的運算式,那么其依賴的回應式資料也任意變得更多,當其中任何一個依賴項變化時整個運算式都需要重新計算.
let price = computed(()=>{
let basePrice = manufactureCost / (1 - profitMargin)
return (
basePrice -
basePrice * (discountPercent || 0)
)
})
當 manufactureCost、profitMargin、discountPercent 中任何一個變化時都會重新計算整個 price,
但是如果我們改成下面這樣
let basePrice = computed(() => manufactureCost / (1 - profitMargin))
let discount = computed(() => basePrice * (discountPercent || 0))
let finalPrice = computed(() => basePrice - discount)
如果當 discountPercent 變化時,只會 重新計算 discount 和 finalPrice,由于 computed 的快取特性,不會重新計算 basePrice.
functional 函式式組件(Vue2)
注意:這僅僅在 Vue2 中被作為一種優化手段,在 3.x 中,有狀態組件和函式式組件之間的性能差異已經大大減少,并且在大多數用例中是微不足道的,因此,在 SFC 上使用 functional 的開發人員的遷移路徑是洗掉該 attribute,并將 props 的所有參考重命名為 $props,將 attrs 重命名為 $attrs,
優化前:
<template>
<div >
<div v-if="value" ></div>
<section v-else ></section>
</div>
</template>
<script>
export default {
props: ['value'],
}
</script>
優化后:
<template functional>
<div >
<div v-if="props.value" ></div>
<section v-else ></section>
</div>
</template>
<script>
export default {
props: ['value'],
}
</script>
- 沒有this(沒有實體)
- 沒有回應式資料
拆分組件
什么?你寫的一個vue檔案有一千多行代碼???
合理的拆分組件不僅僅可以優化性能,還能夠讓代碼更清晰可讀,單一功能原則嘛.
優化前:
<template>
<div :style="{ opacity: number / 300 }">
<div>{{ heavy() }}</div>
</div>
</template>
<script>
export default {
props: ['number'],
methods: {
heavy () { /* HEAVY TASK */ }
}
}
</script>
優化后:
<template>
<div :style="{ opacity: number / 300 }">
<ChildComp/>
</div>
</template>
<script>
export default {
props: ['number'],
components: {
ChildComp: {
methods: {
heavy () { /* HEAVY TASK */ }
},
render (h) {
return h('div', this.heavy())
}
}
}
}
</script>
由于 Vue 的更新是組件粒度的,雖然每一幀都通過資料修改導致了父組件的重新渲染,但是 ChildComp 卻不會重新渲染,因為它的內部也沒有任何回應式資料的變化,所以優化后的組件不會在每次渲染都執行耗時任務.
使用區域變數
優化前:
<template>
<div :style="{ opacity: start / 300 }">{{ result }}</div>
</template>
<script>
import { heavy } from '@/utils'
export default {
props: ['start'],
computed: {
base () { return 42 },
result () {
let result = this.start
for (let i = 0; i < 1000; i++) {
result += heavy(this.base)
}
return result
}
}
}
</script>
優化后:
<template>
<div :style="{ opacity: start / 300 }">
{{ result }}
</div>
</template>
<script>
import { heavy } from '@/utils'
export default {
props: ['start'],
computed: {
base () { return 42 },
result () {
const base = this.base
let result = this.start
for (let i = 0; i < 1000; i++) {
result += heavy(base)
}
return result
}
}
}
</script>
這里主要是優化前后的組件的計算屬性 result 的實作差異,優化前的組件多次在計算程序中訪問 this.base,而優化后的組件會在計算前先用區域變數 base,快取 this.base,后面直接訪問 base,
那么為啥這個差異會造成性能上的差異呢,原因是你每次訪問 this.base 的時候,由于 this.base 是一個回應式物件,所以會觸發它的 getter,進而會執行依賴收集相關邏輯代碼,類似的邏輯執行多了,像示例這樣,幾百次回圈更新幾百個組件,每個組件觸發 computed 重新計算,然后又多次執行依賴收集相關邏輯,性能自然就下降了,
從需求上來說,this.base 執行一次依賴收集就夠了,把它的 getter 求值結果回傳給區域變數 base,后續再次訪問 base 的時候就不會觸發 getter,也不會走依賴收集的邏輯了,性能自然就得到了提升,
使用 KeepAlive
在一些渲染成本比較高的組件需要被經常切換時,可以使用 keep-alive 來快取這個組件.
而在使用 keep-alive 后,被 keep-alive 包裹的組件在經過第一次渲染后,的 vnode 以及 DOM 都會被快取起來,然后再下一次再次渲染該組件的時候,直接從快取中拿到對應的 vnode 和 DOM,然后渲染,并不需要再走一次組件初始化,render 和 patch 等一系列流程,減少了 script 的執行時間,性能更好,
注意:濫用 keep-alive 只會讓你的應用變得更加卡頓,因為他會長期占用較大的記憶體.
事件的銷毀
當一個組件被銷毀時,我們應該清除組件中添加的 全域事件 和 定時器 等來防止記憶體泄漏.
Vue3 的 HOOK 可以讓我們將事件的宣告和銷毀寫在一起,更加可讀.
function scrollFun(){ /* ... */}
document.addEventListener("scroll", scrollFun)
onBeforeUnmount(()=>{
document.removeEventListener("scroll", scrollFun)
})
Vue2 依然可以通過 $once 來做到這樣的效果,當然你也可以在 optionsAPI beforeDestroy 中銷毀事件,但是我更加推薦前者的寫法,因為后者會讓相同功能的代碼更分散.
function scrollFun(){ /* ... */}
document.addEventListener("scroll", scrollFun)
this.$once('hook:beforeDestroy', ()=>{
document.removeEventListener("scroll", scrollFun)
})
function scrollFun(){ /* ... */}
export default {
created() {
document.addEventListener("scroll", scrollFun)
},
beforeDestroy(){
document.removeEventListener("scroll", scrollFun)
}
}
圖片加載
圖片懶加載:適用于頁面上有較多圖片且并不是所有圖片都在一屏中展示的情況,vue-lazyload 插件給我們提供了一個很方便的圖片懶加載指令 v-lazy.
但是并不是所有圖片都適合使用懶加載,例如 banner、相冊等 更加推薦使用圖片預加載技術,將當前展示圖片的前一張和后一張優先下載,
使用合適的圖片型別
使用webp格式:這個沒什么好說的,大家都知道WebP的優勢體現在它具有更優的影像資料壓縮演算法,能帶來更小的圖片體積,而且擁有肉眼識別無差異的影像質量;同時具備了無損和有損的壓縮模式、Alpha 透明以及影片的特性,在 JPEG 和 PNG 上的轉化效果都相當優秀、穩定和統一,
使用交錯GIF或者是漸進JPEG:還有一種優化用戶體驗的方式,就是使用交錯GIF或者是漸進(Progressive Encoding)JPEG的圖片,漸進JPEG檔案首先是模糊的,然后漸漸清晰起來,
Baseline JPEG 和 Progressive JPEG 的區別:
-
Baseline JPEG

-
Progressive JPEG

JPEG檔案格式有兩種保存方式,他們是Baseline JPEG和Progressive JPEG,
兩種格式有相同尺寸以及影像資料,他們的擴展名也是相同的,唯一的區別是二者顯示的方式不同,
Progressive JPEG的優點:
- 用戶體驗 一個以progressive方式編碼的jpeg檔案,在瀏覽器上的渲染方式是由模糊到清晰的,用戶能在漸變的影像當中獲得所需資訊的反饋,如果內容不是用戶所期待的,用戶就能提前前往新的頁面,
- 檔案大小 有實驗證明,在JPEG檔案小于10KB的時候,使用標準型編碼(Huffman表已經被優化)的JPEG檔案要小于使用漸變式編碼的JPEG檔案(發生概率為75%),當檔案大于10KB時,漸變式編碼的JPEG檔案有94%的概率擁有比標準編碼的檔案更小的體積,
減少不必要的回應式資料
大家都知道vue中回應式資料需要額外的對其系結get、get處理函式,如果你的某些資料不會發生變化或者你不希望它的變化會導致任何副作用(更新視圖或者其他),一般我會這樣定義他,
export default {
data() {
this.version = '10'; // 不會被做回應式處理
return {
/* ... */
}
}
}
Tips: 其實這種方式并不是最好的,因為這會將資料直接系結到vue實體上,而vue更希望資料能夠統一在data中,然后通過代理到vue實體上的方式來訪問,所以更好的方式應該是對data中的資料進行凍結,
在Vue3中無法通過以上方式來解決,因為Proxy代理的粒度是整個物件而不是某一個屬性,
采用合理的資料處理演算法
這個相對比較考驗資料結構和演算法的功底.
例如一個將陣列轉化為多級結構的方法.
/**
* 陣列轉樹形結構,時間復雜度O(n)
* @param list 陣列
* @param idKey 元素id鍵
* @param parIdKey 元素父id鍵
* @param parId 第一級根節點的父id值
* @return {[]}
*/
function listToTree (list,idKey,parIdKey,parId) {
let map = {};
let result = [];
let len = list.length;
// 構建map
for (let i = 0; i < len; i++) {
//將陣列中資料轉為鍵值對結構 (這里的陣列和obj會相互參考,這是演算法實作的重點)
map[list[i][idKey]] = list[i];
}
// 構建樹形陣列
for(let i=0; i < len; i++) {
let itemParId = list[i][parIdKey];
// 頂級節點
if(itemParId === parId) {
result.push(list[i]);
continue;
}
// 孤兒節點,舍棄(不存在其父節點)
if(!map[itemParId]){
continue;
}
// 將當前節點插入到父節點的children中(由于是參考資料型別,obj中對于節點變化,result中對應節點會跟著變化)
if(map[itemParId].children) {
map[itemParId].children.push(list[i]);
} else {
map[itemParId].children = [list[i]];
}
}
return result;
}
其他
除了上面說的方法以外還有很多優化技巧,只是我在專案并不是太常用??
- 凍結物件(避免不需要回應式的資料變成回應式)
- 長串列渲染-分批渲染
- 長串列渲染-動態渲染
- ...
首屏/體積優化
我在專案中關于首屏優化主要有以下幾個優化方向:
- 體積
- 代碼分割
- 網路
體積優化
-
壓縮打包代碼:
webpack和vite的生產環境打包默認就會壓縮你的代碼,這個一般不需要特殊處理,webpack也可以通過對應的壓縮插件手動實作, -
取消
source-map: 可以查看你的打包產物中是否有 .map 檔案,如果有你可以將source-map的值設定為false或者空來關閉代碼映射(這個占用的體積是真的大), -
打包啟用
gizp壓縮: 這個需要服務器也開啟允許gizp傳輸,不然啟用了也沒啥用(webpack有對應的gzip壓縮插件,不太版本的webpack壓縮插件可能不同,建議先到官網查詢),
代碼分割
代碼分割的作用的將打包產物分割為一個一個的小產物,其依賴 esModule,所以當你使用 import() 函式來匯入一個檔案或者依賴,那么這個檔案或者依賴就會被單獨打包為一個小產物,路由懶加載 和 異步組件 都是使用這個原理,
- 路由懶加載
- 異步組件
對于 UI庫 我一般不會使用按需加載組件,而是比較喜歡 CDN 引入的方式來優化,
網路
CDN: 首先就是上面的說的 CDN 引入把,開發階段使用本地庫,通過配置 外部擴展(Externals) 打包時來排除這些依賴,然后在 html 檔案中通過 CDN 的方式來引入它們,
Server Push: HTTP2已經相對成熟了;經過上面的 CDN 引入,我們可以對網站使用 HTTP2 的 Server Push 功能來讓瀏覽器提前加載 這些 CDN 和 其他檔案,
開啟 gzip: 這個上面已經說過了,其原理就是當客戶端和服務端都支持 gzip 傳輸時,服務端會優先發送經過 gzip 壓縮過的檔案,然后客戶端接收到在進行解壓,
開啟快取: 一般我使用的是協商快取,但是這并不適用于所有情況,例如對于使用了 Server Push 的檔案,就不能隨意的修改其檔案名,所以我一般還會將生產的主要檔案固定檔案名,
用戶優化
我們可以在核心檔案加載完成之前,通過展示loading或者骨架屏等方式來提升用戶體驗,即可縮短白屏時間,
但是需要注意的是,頁面剛開始加載時有許多資源需要加載,如果將loading相關的資源放到dom后的話,有可能會導致loading的資源被其他資源阻塞,
所以推薦loading相關的css或者js代碼最好是行內到html中的頭部,這樣即可保證展示loading時對應的css和js已經加載完成,并且不推薦loading中使用高性能或者高網路消化的邏輯,這樣會延長后面其他資源的決議或者加載時間,
原文作者: 月夕
原文地址: 我在專案中用實際用到的22個Vue優化技巧
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/543458.html
標籤:其他
下一篇:淺談JS詞法環境
