開場白:
個人認為性能優(yōu)化可以從三個方面來進行:
- 代碼層面的優(yōu)化
- 項目大包的優(yōu)化
- 項目部署的優(yōu)化
1. 優(yōu)化循環(huán)
大多數(shù)循環(huán)使用一個從0開始、增加到某個特定的迭代器。在很多情況下,從最大值開始在循環(huán)中不斷減值的迭代器更加高效。使用后測試循環(huán)——最常用for循環(huán)和while循環(huán)都是前測試循環(huán),do-while后測試循環(huán),可以避免最初終止條件的計算。
for(var i = 0; i < values.length; i++){
process(values[i]);
}
可改為減值,這個過程中將終止條件從value.length的O(n)調(diào)用簡化成了0的O(1)調(diào)用。
for(var i = values.length - 1; i >= 0; i--){
process(values[i]);
}
后測試循環(huán)主要的是將終止條件和自減操作符組合成了單個語句。
var i = values.length - 1;
if ( i > -1) {
do {
process(value[i]);
} while(--i >= 0);
}
請記?。菏褂谩昂鬁y試循環(huán)”時,必須確保要處理的值至少有一個??諗?shù)組會導致多余的一次循環(huán),而“前測試循環(huán)”則可以避免
2. 數(shù)組分塊
應用場景:如果處理的項目在運行前不可知,完成process()需要100ms,只有2個項目的數(shù)組可能不會造成什么影響,但10個的數(shù)組可能會導致腳本要運營一秒鐘才能完成。如果這些處理不必須同步完成;數(shù)據(jù)也不必須按順序完成,可使用定時器分割這個循環(huán),實現(xiàn) “數(shù)組分塊”。
// @array->要處理的數(shù)組;@process->用于處理項目的函數(shù);@context->可選的運行該函數(shù)的環(huán)境
function chunk (array, process, context) {
setTimeout(function() {
// 取出下一個條目并處理
var item = array.shift();
process.call(context, item);
// 若還有條目,再設(shè)置另一個定時器
if (array.length > 0) {
setTimeout(arguments.callee, 100);
}
})
}
需要擔心的地方是,傳遞給chunk()的數(shù)組是用作一個隊列的,因此當處理數(shù)據(jù)的同時,數(shù)組中的條目也在改變,如果想保持原數(shù)組不變,可將該數(shù)組克隆傳給chunk(),當不傳遞任何參數(shù)調(diào)用某個數(shù)組的concat()方法時,將返回和原來數(shù)組中項目一樣的數(shù)組。如:
chunk(arr.concat(), fun1);
一旦某個函數(shù)需要花費50ms以上的時間完成,那么最好看看能否將任務分割為一系列可以使用定時器的小任務。
3. switch語法較快
如果有一系列復雜的if-else語句,可以轉(zhuǎn)換成單個switch語句可以得到更快的代碼。
4. 最小化語句數(shù)
4.1 多個變量聲明
這種優(yōu)化非常容易做,而且要比單個變量分別聲明快
// 不推薦
var count = 5;
var color = 'blue';
var values = [1,2,3];
// 推薦
var count = 5,
color = 'blue',
values = [1,2,3];
4.2 插入迭代值
當使用迭代值(也就是在不同位置進行增加或減少的值)的時候,盡可能合并語句。
// 不推薦
var name = values[i];
i++;
// 推薦
var name = values[i++];
4.3 使用數(shù)組和對象字面量
減少語句量
// 不推薦
var arr = new Array();
arr[0] = 1;
arr[1] = 2;
arr[2] = 3;
var obj = new Object();
obj.id = '';
obj.name = '';
// 推薦
var arr = [1,2,3];
var obj = {
id = '',
name = ''
}
5. 利用v-if和v-show減少初始化渲染和切換渲染的性能開銷
在頁面加載時,v-if更適合添加不經(jīng)常改變的場景,因為它切換開銷相對比較大,而v-show適用于頻繁切換條件。
原理:
v-if 綁定值為false時,初始渲染時,不會渲染其條件塊。
v-if 綁定值,在true和false之間切換時,會銷毀和重新渲染其條件塊。
v-show 綁定值不管為true還是為false,初始渲染時,總是會渲染其條件塊。
v-show 綁定值,在true和false之間切換時,不會銷毀和重新渲染其條件塊,只是用display:none樣式來控制其顯示隱藏。
6. computed、watch、methods區(qū)分使用場景
對于有些需求,computed、watch、methods都可以實現(xiàn),但是還是要區(qū)分一下使用場景。用錯場景雖然功能實現(xiàn)了但是影響了性能。
- computed: 一個數(shù)據(jù)受多個數(shù)據(jù)影響的。
該數(shù)據(jù)要經(jīng)過性能開銷比較大的計算,如它需要遍歷一個巨大的數(shù)組并做大量的計算才能得到,原因就是計算屬性是基于它的依賴緩存,只有它計算時依賴的數(shù)據(jù)發(fā)現(xiàn)變化時才會重新計算,否則直接返回緩存值。 - methods: 希望數(shù)據(jù)是實時更新,不需要緩存。
- watch: 一個數(shù)據(jù)影響多個數(shù)據(jù)的。
當數(shù)據(jù)變化時,需要執(zhí)行異步或開銷較大的操作時。如果數(shù)據(jù)變化時請求一個接口。
如果深度監(jiān)聽一個對象中的某一個屬性,不用deep:true性能更好。
7. 提前處理好數(shù)據(jù)解決v-if和v-for必須同級的問題
因為當Vue處理指令時,v-for比v-if具有更高的優(yōu)先級,意味著v-if將分別重復運行于每個v-for循環(huán)中。 可以在computed中提前把要v-for的數(shù)據(jù)中v-if的數(shù)據(jù)項給過濾處理了。
<!-- 不推薦 -->
<template>
<div>
<div v-for="item in userList"
:key="item.id"
v-if="item.age > 18">
{{ item.name }}
</div>
</div>
</template>
<!-- 推薦 -->
<template>
<div>
<div v-for="item in userComputedList"
:key="item.id">
{{ item.name }}
</div>
</div>
</template>
export default{
computed:{
userComputedList:function(){
return this.userList.filter(item => {
return item.age > 18
})
}
}
}
8. 給v-for循環(huán)項加上key提高diff計算速度
- 為什么加key會提高diff計算速度。
經(jīng)過舊頭新頭、舊尾新尾、舊頭新尾、舊尾新頭四次交叉比對后,都沒有匹配到值得比對的節(jié)點,這時如果新節(jié)點有key的話??梢酝ㄟ^map直接獲得值得對比的舊節(jié)點的下標,如果沒有key的話,就要通過循環(huán)舊節(jié)點數(shù)組用sameVnode方法判斷新節(jié)點和該舊節(jié)點是否值得比較,值得就返回該舊節(jié)點的下標。顯然通過map比通過循環(huán)數(shù)組的計算速度來的快。 - 什么是diff計算。
對于渲染watcher觸發(fā)時會執(zhí)行vm.update(vm.render(), hydrating),在vm.undata方法中會調(diào)用vm._patch,而vm.__patch指向patch方法,diff計算是指在調(diào)用patch方法開始,用sameVnode方法判斷節(jié)點是否值得比較,若不值得直接新節(jié)點替換舊節(jié)點后結(jié)束。值得對比進入patchVnode方法,分別處理一下幾種情況,若新舊節(jié)點都有文本節(jié)點,新節(jié)點下的文本節(jié)點直接替換舊節(jié)點下的文本節(jié)點,如果新節(jié)點有子節(jié)點,舊節(jié)點沒有子節(jié)點,那么直接把新節(jié)點查到舊節(jié)點的父級中,如果新節(jié)點沒有子節(jié)點,舊節(jié)點有子節(jié)點,那么舊節(jié)點的父級下的子節(jié)點都刪了。如果新舊節(jié)點都有子節(jié)點,進入updateChildren方法,通過舊頭新頭、舊尾新尾、舊頭新尾、舊尾新頭四次交叉比對,如果值得對比再進入patchVnode方法,如果都不值得對比,有key用map獲得值得對比的舊節(jié)點,沒有key通過循環(huán)舊節(jié)點獲得值得對比的舊節(jié)點。當新節(jié)點都對比完,舊節(jié)點還沒對比完,將還沒對比完的舊節(jié)點刪掉。當舊節(jié)點都對比完,新節(jié)點還沒對比完,將新節(jié)點添加到最后一個對比過的新節(jié)點后面,完成diff計算。
:key的選擇不推薦使用index會導致不好的緩存
9. 利用v-once處理只會渲染一次的元素或組件
只渲染元素和組件一次。隨后的重新渲染,元素/組件及其所有的子節(jié)點將被視為靜態(tài)內(nèi)容并跳過。這可以用于優(yōu)化更新性能。
例如某個頁面是合同范文,里面大部分內(nèi)容從服務端獲取且是固定不變,只有姓名、產(chǎn)品、金額等內(nèi)容會變動。這時就可以把v-once添加到那些包裹固定內(nèi)容的元素上,當生成新的合同可以跳過那些固定內(nèi)容,只重新渲染姓名、產(chǎn)品、金額等內(nèi)容即可。
和v-if一起使用時,v-once不生效。在v-for循環(huán)內(nèi)的元素或組件上使用,必須加上key。
10. 利用Object.freeze()凍結(jié)不需要響應式變化的數(shù)據(jù)
Vue初始化過程中,會把data傳入observe函數(shù)中進行數(shù)據(jù)劫持,把data中的數(shù)據(jù)都轉(zhuǎn)換成響應式的。
在observe函數(shù)內(nèi)部調(diào)用defineReactive函數(shù)處理數(shù)據(jù),配置getter/setter屬性,轉(zhuǎn)成響應式,如果使用Object.freeze()將data中某些數(shù)據(jù)凍結(jié)了,也就是將其configurable屬性(可配置)設(shè)置為false。
defineReactive函數(shù)中有段代碼,檢測數(shù)據(jù)上某個key對應的值的configurable屬性是否為false,若是就直接返回,若不是繼續(xù)配置getter/setter屬性。
// 凍結(jié)后的對象不會被修改,不能對這個對象進行添加新增屬性,不能刪除已有屬性,不能修改該對象已有屬性的可枚舉性,可配置性,可寫性。
const obj1={
age: 18
};
const obj2 = Object.freeze(obj1); // 凍結(jié)這個對象
obj2.age = 20;
console.log(obj2.age); // 18
在項目中如果遇到不需要響應式變化的數(shù)據(jù),可以用Object.freeze()把該數(shù)據(jù)凍結(jié)了,可以跳過初始化時數(shù)據(jù)劫持的步驟,大大提高初次渲染速度。
這時你可能想到用const
看一個例子你就會發(fā)現(xiàn)不同:
const a = {name:'11'}
a.name = '22';
console.log(a); // {name: '22'}
11. 提前過濾掉非必須數(shù)據(jù),優(yōu)化data選項中的數(shù)據(jù)結(jié)構(gòu)
Vue初始化時,會將選項data傳入observe函數(shù)中進行數(shù)據(jù)劫持。
接收服務端傳來的數(shù)據(jù),都會有一些渲染頁面時用不到的數(shù)據(jù)。服務端的慣例,寧可多傳也不會少傳。
所以要先把服務端傳來的數(shù)據(jù)中那些渲染頁面用不到的數(shù)據(jù)先過濾掉。然后再賦值到data選項中??梢员苊馊ソ俪帜切┓卿秩卷撁嫘枰臄?shù)據(jù),減少循環(huán)和遞歸調(diào)用,從而提高渲染速度。
12. 避免在v-for循環(huán)中讀取data中數(shù)組類型的數(shù)據(jù)
舉個簡單的栗子,表格中每行有兩個輸入框,分別可以輸入駕駛員和電話,代碼這么實現(xiàn)。
<template>
<div>
<el-table :data="tableData">
<el-table-column prop="carno" label="車牌號"></el-table-column>
<el-table-column prop="cartype" label="車型"></el-table-column>
<el-table-column label="駕駛員">
<template slot-scope="{row, column, $index}">
<el-input v-model="driverList[$index].phone"></el-input>
</template>
</el-table-column>
</el-table>
</div>
</template>
假設(shè)表格有500條數(shù)據(jù),那么讀取driverList共500次,每次都讀取driverList都會進入dependArray(value)中,總共要循環(huán)500*500=25萬次,若有分頁,每次切換頁碼,都會至少循環(huán)25萬次。
如果我們在從服務獲取到數(shù)據(jù)后,做了如下預處理,在賦值給this.tableData,會是怎么樣?
res.data.forEach(item => {
item.name='';
item.phone='';
})
<!-- 模板這樣實現(xiàn) -->
<template>
<div>
<el-table :data="tableData">
<el-table-column prop="carno" label="車牌號"></el-table-column>
<el-table-column prop="cartype" label="車型"></el-table-column>
<el-table-column label="駕駛員">
<template slot-scope="{row}">
<el-input v-model="row.phone"></el-input>
</template>
</el-table-column>
</el-table>
</div>
</template>
也可以實現(xiàn)需求,渲染過程中求值時也不會進入dependArray(value)中,也不會造成25萬次的不必要的循環(huán)。大大提高了性能。
13. 防抖和節(jié)流
節(jié)流是一定時間內(nèi)執(zhí)行一次函數(shù),多用在scroll事件上;
防抖是在一定時間內(nèi)執(zhí)行最后一次函數(shù),多用在input輸入、提交上。
14. 圖片大小優(yōu)化和懶加載
關(guān)于圖片大小的優(yōu)化,可以用image-webpack-loader進行壓縮圖片,在webpack插件中配置,具體可以看本文中這點。
關(guān)于圖片懶加載,可以用vue-lazyload插件實現(xiàn)。
執(zhí)行命令npm install vue-lazyload —save安裝vue-lazyload插件。在main.js中引入配置。
import VueLazyload from 'vue-lazyload';
Vue.use(VueLazyload, {
preLoad: 1.3, // 預載高度比例
error:'dist/error.png', // 加載失敗顯示圖片
loading: 'dist/loading.git', // 加載過程中顯示圖片
attempt: 1 // 嘗試次數(shù)
})
// 在項目中使用
<img v-lazy="/static/img/1.png">
15. 利用掛在節(jié)點會被替換的特性優(yōu)化白屏問題
import Vue from 'vue'
import App from './App.vue'
new Vue({
render: h =>h(App)
}).$mount('#app)
Vue 選項中的 render 函數(shù)若存在,則 Vue 構(gòu)造函數(shù)不會從 template 選項或通過 el 選項指定的掛載元素中提取出的 HTML 模板編譯渲染函數(shù)。
也就是說渲染時,會直接用render渲染出來的內(nèi)容替換<div id="app"></div>。
Vue項目有個缺點,首次渲染會有一段時間的白屏原因是首次渲染時需要加載一堆資源,如js、css、圖片。很多優(yōu)化策略,最終目的是提高這些資源的加載速度。但是如果遇上網(wǎng)絡慢的情況,無論優(yōu)化到極致還是需要一定加載時間,這時就會出現(xiàn)白屏現(xiàn)象。
首先加載是index.html頁面,其是沒有內(nèi)容,就會出現(xiàn)白屏。如果<div id="app"></div>里面有內(nèi)容,就不會出現(xiàn)白屏。所以我們可以在<div id="app"></div>里添加首屏的靜態(tài)頁面。等真正的首屏加載出來后就會把<div id="app"></div>這塊結(jié)構(gòu)都替換掉,給人一種視覺上的誤差,就不會產(chǎn)生白屏。
16. 初始化頁面閃動
webpack,vue-router
v-cloak css:[v-cloak]:display:none
17. 組件庫的按需引入
組件庫按需引入的方法,一般文檔都會介紹。
如element UI庫,用babel-plugin-component插件實現(xiàn)按需引入。
執(zhí)行命令npm install babel-plugin-component —save-dev,安裝插件。
在根目錄下.babelrc.js文件中按如下配置。
{
"presets":[["es2015", {"modules":false}]],
"plugins":[
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
其中l(wèi)ibraryName為組件庫的名稱,styleLibraryName為組件庫打包后樣式存放的文件夾名稱。 在main.js中就可以按需引入。
import Vue from 'vue';
import {Button, Select} from 'element-ui';
Vue.use(Button);
Vue.use(Select);
其實babel-plugin-component插件是element用babel-plugin-import插件改造后特定給element UI使用。一般的組件庫還是babel-plugin-import插件實現(xiàn)按需引入。
執(zhí)行命令npm install babel-plugin-import —save-dev,安裝插件。
在根目錄下.babelrc.js文件中按如下配置。
{
"plugins":[
["import", {
"libraryName": "vant",
"libraryDiectory": "es",
"style": true
}]
]
}
其中l(wèi)ibraryName為組件庫的名稱,libraryDirectory表示從庫的package.json的main入口文件或者module入口文件所在文件夾名稱,否則默認為lib。
在介紹style選項配置之前。先看一下Vant 組件庫打包后生成文件的結(jié)構(gòu)和內(nèi)容。
style為true時,會按需在項目中引入對應style文件中的index.js。
style為css時,會按需在項目中引入對應style文件中的less.js。
style為Function,babel-plugin-import將自動導入文件路徑等于函數(shù)返回值的文件。
別走,還有后續(xù)吶······
如果小伙伴們還有其他優(yōu)化性能的idea,歡迎在評論區(qū)留言,分享是種美德,謝謝你的貢獻。
PS:寫作不易,如要轉(zhuǎn)裁,請標明轉(zhuǎn)載出處。