开场白:
个人认为性能优化可以从三个方面来进行:
- 代码层面的优化
- 项目大包的优化
- 项目部署的优化
1. 优化循环
大多数循环使用一个从0开始、增加到某个特定的迭代器。在很多情况下,从最大值开始在循环中不断减值的迭代器更加高效。使用后测试循环——最常用for循环和while循环都是前测试循环,do-while后测试循环,可以避免最初终止条件的计算。
for(var i = 0; i < values.length; i++){
process(values[i]);
}
可改为减值,这个过程中将终止条件从value.length
的O(n)
调用简化成了0
的O(1)
调用。
for(var i = values.length - 1; i >= 0; i--){
process(values[i]);
}
后测试循环主要的是将终止条件和自减操作符组合成了单个语句。
var i = values.length - 1;
if ( i > -1) {
do {
process(value[i]);
} while(--i >= 0);
}
请记住:使用“后测试循环”时,必须确保要处理的值至少有一个。空数组会导致多余的一次循环,而“前测试循环”则可以避免
2. 数组分块
应用场景:如果处理的项目在运行前不可知,完成process()
需要100ms
,只有2个项目的数组可能不会造成什么影响,但10个的数组可能会导致脚本要运营一秒钟才能完成。如果这些处理不必须同步完成;数据也不必须按顺序完成,可使用定时器分割这个循环,实现 “数组分块”。
// @array->要处理的数组;@process->用于处理项目的函数;@context->可选的运行该函数的环境
function chunk (array, process, context) {
setTimeout(function() {
// 取出下一个条目并处理
var item = array.shift();
process.call(context, item);
// 若还有条目,再设置另一个定时器
if (array.length > 0) {
setTimeout(arguments.callee, 100);
}
})
}
需要担心的地方是,传递给chunk()
的数组是用作一个队列的,因此当处理数据的同时,数组中的条目也在改变,如果想保持原数组不变,可将该数组克隆传给chunk()
,当不传递任何参数调用某个数组的concat()
方法时,将返回和原来数组中项目一样的数组。如:
chunk(arr.concat(), fun1);
一旦某个函数需要花费50ms以上的时间完成,那么最好看看能否将任务分割为一系列可以使用定时器的小任务。
3. switch语法较快
如果有一系列复杂的if-else
语句,可以转换成单个switch
语句可以得到更快的代码。
4. 最小化语句数
4.1 多个变量声明
这种优化非常容易做,而且要比单个变量分别声明快
// 不推荐
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 使用数组和对象字面量
减少语句量
// 不推荐
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更适合添加不经常改变的场景,因为它切换开销相对比较大,而v-show适用于频繁切换条件。
原理:
v-if 绑定值为false时,初始渲染时,不会渲染其条件块。
v-if 绑定值,在true和false之间切换时,会销毁和重新渲染其条件块。
v-show 绑定值不管为true还是为false,初始渲染时,总是会渲染其条件块。
v-show 绑定值,在true和false之间切换时,不会销毁和重新渲染其条件块,只是用display:none样式来控制其显示隐藏。
6. computed、watch、methods区分使用场景
对于有些需求,computed、watch、methods都可以实现,但是还是要区分一下使用场景。用错场景虽然功能实现了但是影响了性能。
- computed: 一个数据受多个数据影响的。
该数据要经过性能开销比较大的计算,如它需要遍历一个巨大的数组并做大量的计算才能得到,原因就是计算属性是基于它的依赖缓存,只有它计算时依赖的数据发现变化时才会重新计算,否则直接返回缓存值。 - methods: 希望数据是实时更新,不需要缓存。
- watch: 一个数据影响多个数据的。
当数据变化时,需要执行异步或开销较大的操作时。如果数据变化时请求一个接口。
如果深度监听一个对象中的某一个属性,不用deep:true
性能更好。
7. 提前处理好数据解决v-if和v-for必须同级的问题
因为当Vue处理指令时,v-for
比v-if
具有更高的优先级,意味着v-if
将分别重复运行于每个v-for
循环中。 可以在computed
中提前把要v-for
的数据中v-if
的数据项给过滤处理了。
<!-- 不推荐 -->
<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循环项加上key提高diff计算速度
- 为什么加key会提高diff计算速度。
经过旧头新头、旧尾新尾、旧头新尾、旧尾新头四次交叉比对后,都没有匹配到值得比对的节点,这时如果新节点有key的话。可以通过map直接获得值得对比的旧节点的下标,如果没有key的话,就要通过循环旧节点数组用sameVnode方法判断新节点和该旧节点是否值得比较,值得就返回该旧节点的下标。显然通过map比通过循环数组的计算速度来的快。 - 什么是diff计算。
对于渲染watcher触发时会执行vm.update(vm.render(), hydrating),在vm.undata方法中会调用vm._patch,而vm.__patch指向patch方法,diff计算是指在调用patch方法开始,用sameVnode方法判断节点是否值得比较,若不值得直接新节点替换旧节点后结束。值得对比进入patchVnode方法,分别处理一下几种情况,若新旧节点都有文本节点,新节点下的文本节点直接替换旧节点下的文本节点,如果新节点有子节点,旧节点没有子节点,那么直接把新节点查到旧节点的父级中,如果新节点没有子节点,旧节点有子节点,那么旧节点的父级下的子节点都删了。如果新旧节点都有子节点,进入updateChildren方法,通过旧头新头、旧尾新尾、旧头新尾、旧尾新头四次交叉比对,如果值得对比再进入patchVnode方法,如果都不值得对比,有key用map获得值得对比的旧节点,没有key通过循环旧节点获得值得对比的旧节点。当新节点都对比完,旧节点还没对比完,将还没对比完的旧节点删掉。当旧节点都对比完,新节点还没对比完,将新节点添加到最后一个对比过的新节点后面,完成diff计算。
:key的选择不推荐使用index会导致不好的缓存
9. 利用v-once处理只会渲染一次的元素或组件
只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。
例如某个页面是合同范文,里面大部分内容从服务端获取且是固定不变,只有姓名、产品、金额等内容会变动。这时就可以把v-once
添加到那些包裹固定内容的元素上,当生成新的合同可以跳过那些固定内容,只重新渲染姓名、产品、金额等内容即可。
和v-if
一起使用时,v-once
不生效。在v-for
循环内的元素或组件上使用,必须加上key
。
10. 利用Object.freeze()冻结不需要响应式变化的数据
Vue初始化过程中,会把data传入observe函数中进行数据劫持,把data中的数据都转换成响应式的。
在observe函数内部调用defineReactive函数处理数据,配置getter/setter属性,转成响应式,如果使用Object.freeze()将data中某些数据冻结了,也就是将其configurable属性(可配置)设置为false。
defineReactive函数中有段代码,检测数据上某个key对应的值的configurable属性是否为false,若是就直接返回,若不是继续配置getter/setter属性。
// 冻结后的对象不会被修改,不能对这个对象进行添加新增属性,不能删除已有属性,不能修改该对象已有属性的可枚举性,可配置性,可写性。
const obj1={
age: 18
};
const obj2 = Object.freeze(obj1); // 冻结这个对象
obj2.age = 20;
console.log(obj2.age); // 18
在项目中如果遇到不需要响应式变化的数据,可以用Object.freeze()把该数据冻结了,可以跳过初始化时数据劫持的步骤,大大提高初次渲染速度。
这时你可能想到用const
看一个例子你就会发现不同:
const a = {name:'11'}
a.name = '22';
console.log(a); // {name: '22'}
11. 提前过滤掉非必须数据,优化data选项中的数据结构
Vue初始化时,会将选项data传入observe函数中进行数据劫持。
接收服务端传来的数据,都会有一些渲染页面时用不到的数据。服务端的惯例,宁可多传也不会少传。
所以要先把服务端传来的数据中那些渲染页面用不到的数据先过滤掉。然后再赋值到data选项中。可以避免去劫持那些非渲染页面需要的数据,减少循环和递归调用,从而提高渲染速度。
12. 避免在v-for循环中读取data中数组类型的数据
举个简单的栗子,表格中每行有两个输入框,分别可以输入驾驶员和电话,代码这么实现。
<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>
假设表格有500条数据,那么读取driverList共500次,每次都读取driverList都会进入dependArray(value)中,总共要循环500*500=25万次,若有分页,每次切换页码,都会至少循环25万次。
如果我们在从服务获取到数据后,做了如下预处理,在赋值给this.tableData,会是怎么样?
res.data.forEach(item => {
item.name='';
item.phone='';
})
<!-- 模板这样实现 -->
<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>
也可以实现需求,渲染过程中求值时也不会进入dependArray(value)
中,也不会造成25万次的不必要的循环。大大提高了性能。
13. 防抖和节流
节流是一定时间内执行一次函数,多用在scroll事件上;
防抖是在一定时间内执行最后一次函数,多用在input输入、提交上。
14. 图片大小优化和懒加载
关于图片大小的优化,可以用image-webpack-loader
进行压缩图片,在webpack插件中配置,具体可以看本文中这点。
关于图片懒加载,可以用vue-lazyload
插件实现。
执行命令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 // 尝试次数
})
// 在项目中使用
<img v-lazy="/static/img/1.png">
15. 利用挂在节点会被替换的特性优化白屏问题
import Vue from 'vue'
import App from './App.vue'
new Vue({
render: h =>h(App)
}).$mount('#app)
Vue 选项中的 render 函数若存在,则 Vue 构造函数不会从 template 选项或通过 el 选项指定的挂载元素中提取出的 HTML 模板编译渲染函数。
也就是说渲染时,会直接用render渲染出来的内容替换<div id="app"></div>
。
Vue项目有个缺点,首次渲染会有一段时间的白屏原因是首次渲染时需要加载一堆资源,如js、css、图片。很多优化策略,最终目的是提高这些资源的加载速度。但是如果遇上网络慢的情况,无论优化到极致还是需要一定加载时间,这时就会出现白屏现象。
首先加载是index.html页面,其是没有内容,就会出现白屏。如果<div id="app"></div>
里面有内容,就不会出现白屏。所以我们可以在<div id="app"></div>
里添加首屏的静态页面。等真正的首屏加载出来后就会把<div id="app"></div>
这块结构都替换掉,给人一种视觉上的误差,就不会产生白屏。
16. 初始化页面闪动
webpack,vue-router
v-cloak css:[v-cloak]:display:none
17. 组件库的按需引入
组件库按需引入的方法,一般文档都会介绍。
如element UI库,用babel-plugin-component
插件实现按需引入。
执行命令npm install babel-plugin-component —save-dev
,安装插件。
在根目录下.babelrc.js文件中按如下配置。
{
"presets":[["es2015", {"modules":false}]],
"plugins":[
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
其中libraryName为组件库的名称,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
插件实现按需引入。
执行命令npm install babel-plugin-import —save-dev
,安装插件。
在根目录下.babelrc.js文件中按如下配置。
{
"plugins":[
["import", {
"libraryName": "vant",
"libraryDiectory": "es",
"style": true
}]
]
}
其中libraryName为组件库的名称,libraryDirectory表示从库的package.json的main入口文件或者module入口文件所在文件夹名称,否则默认为lib。
在介绍style选项配置之前。先看一下Vant 组件库打包后生成文件的结构和内容。
style为true时,会按需在项目中引入对应style文件中的index.js。
style为css时,会按需在项目中引入对应style文件中的less.js。
style为Function,babel-plugin-import将自动导入文件路径等于函数返回值的文件。
别走,还有后续呐······
如果小伙伴们还有其他优化性能的idea,欢迎在评论区留言,分享是种美德,谢谢你的贡献。
PS:写作不易,如要转裁,请标明转载出处。
网友评论