浅析vue项目性能优化应该注意的点
最近总结了一下自己在项目中使用到的性能优化手段,这里主要从两个部分来详解vue项目的性能优化:代码层优化、webpack打包优化
一、代码优化
1、v-if 和 v-show
v-if 是懒加载,当状态为 true 时才会加载,并且为 false 时不会占用布局空间;
v-show 是无论状态是 true 或者是 false,都会进行渲染,并且只是简单地基于 CSS 的 display 属性进行切换,并占据布局空间。对于在项目中,需要频繁调用,不需要权限的显示隐藏,可以选择使用 v-show,可以减少系统的切换开销。
在我来看要分两个维度去思考问题:
第一个维度是权限问题,只要涉及到权限相关的展示无疑要用 v-if
,
第二个维度在没有权限限制下根据用户点击的频次选择,频繁切换的使用 v-show
,不频繁切换的使用 v-if
,
这里要说的优化点在于减少页面中 dom 总数,我比较倾向于使用 v-if
,因为减少了 dom 数量,加快首屏渲染,至于性能方面我感觉肉眼看不出来切换的渲染过程,也不会影响用户的体验。
2、不要在模板里面写过多的表达式与判断
v-if="isShow && isAdmin && (a || b)"
,这种表达式虽说可以识别,但是不是长久之计,当看着不舒服时,适当的写到 methods 和 computed 里面封装成一个方法,这样的好处是方便我们在多处判断相同的表达式,其他权限相同的元素再判断展示的时候调用同一个方法即可。
3、v-for为item设置唯一key值
详见之前我总结的这篇博客:图解vue中 v-for 的 :key 的作用,虚拟dom Diff算法
v-for 在列表数据进行遍历渲染时,需要为每一项item设置唯一key值,方便vue.js内部机制精准找到该条列表数据。当state更新时,新的状态值和旧的状态值对比,较快地定位到diff。
循环调用子组件时添加 key:key 可以唯一标识一个循环个体,可以使用例如 item.id
作为 key,假如数组数据是这样的 ['a' , 'b', 'c', 'a']
,使用 :key="item"
显然没有意义,更好的办法就是在循环的时候 (item, index) in arr
,然后 :key="index"
来确保 key 的唯一性
当 Vue.js 用v-for
正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 key 属性。理想的 key 值是每项都有的且唯一的 id。这个特殊的属性相当于 Vue 1.x 的 track-by ,但它的工作方式类似于一个属性,所以你需要用 v-bind 来绑定动态值 。
4、v-for 遍历避免同时使用 v-if
当 v-if 与 v-for 一起使用时,v-for 具有比 v-if 更高的优先级,这意味着 v-if 将分别重复运行于每个 v-for 循环中。所以,不推荐v-if和v-for同时使用,必要情况下可以替换成 computed 属性。
5、computed 和 watch 区分使用场景
computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;
运用场景:
1、当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;
2、当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch。使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
详见之前总结的这2篇博客:理解Vue的计算属性、Vue侦听器watch、vue中watch的用法总结以及报错处理Error in callback for watcher "checkList"
6、长列表性能优化
详见我之前总结的这篇博客:vue利用 object.freeze 提升列表渲染性能
Vue 会通过 Object.defineProperty 对数据进行劫持,来实现视图响应数据的变化,然而有些时候我们的组件就是纯粹的数据展示,不会有任何改变,我们就不需要 Vue 来劫持我们的数据,在大量数据展示的情况下,这能够很明显的减少组件初始化的时间,那如何禁止 Vue 劫持我们的数据呢?可以通过 Object.freeze 方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了。
//Object.freeze 方法冻结对象
this.data = Object.freeze(res.data);
7、事件的销毁
Vue 组件销毁时,会自动清理它与其它实例的连接,解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。例如,当我们执行某个计时器的时候,页面销毁的时候我们肯定要把事件销毁,销毁计时器一般有两种方法,我建议第二种方法。
方法一、在data函数中定义定时器名称,然后在methods中使用定时器,最后在beforeDestroy()生命周期内清除定时器
data(){
return {
timer: null // 定时器名称
}
},
methods: {
this.timer = setInterval(()=>{
// 执行定时器操作
}, 500)
},
beforeDestroy() {
clearInterval(this.timer);
this.timer = null;
}
这个方法有两点不好的地方:
1、它需要在这个组件实例中保存这个 timer,如果可以的话最好只有生命周期钩子可以访问到它。这并不算严重的问题,但是它可以被视为杂物。
2、我们的建立代码独立于我们的清理代码,这使得我们比较难于程序化的清理我们建立的所有东西。
方法二:通过$once这个事件侦听器器在定义完定时器之后的位置来清除定时器
const timer = setInterval(() =>{
// 执行定时器操作
}, 500);
// 通过$once来监听定时器,在beforeDestroy钩子可以被清除。
this.$once('hook:beforeDestroy', () => {
clearInterval(timer);
})
详见我之前总结的这篇博客:VUE @hook浅析(监听子组件的生命周期钩子)
8、细分vue组件,css样式以及mixin使用
在项目开发过程之中,如果把所有的组件的布局写在一个组件中,这样该组件文件大小也会比较大,当数据变更时,由于组件代码比较庞大,vue的数据驱动视图更新比较慢,造成渲染比较慢,造成比较差的体验效果。所以把组件细分,比如一个组件,可以把整个组件细分成头部组件、左侧菜单组件、内容区组件等。能复用的功能一定要封装成公共组件,例如一些弹窗组件。这样,不仅加载速度更快(js文件更小),而且还更好维护。细分vue组件、提取公共css样式、使用mixin均是这个效果。
详见我之前总结的这篇博客:VUE的mixin混入解析
9、对路由组件进行懒加载
这里的懒加载是指在访问到对应的组件时才加载它,首屏的时候不加载。这里实现的方法很简单,只要将以前直接import组件的方式改为:const Login = () => import('@/pages/Login’);即可。
Vue 是单页面应用,可能会有很多的路由引入 ,这样使用 webpcak 打包后的文件很大,当进入首页时,加载的资源过多,页面会出现白屏的情况,不利于用户体验。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就更加高效了。这样会大大提高首屏显示的速度,但是可能其他的页面的速度就会降下来。
import Vue from 'vue'
import Router from 'vue-router'
// import HelloWorld from '@/components/HelloWorld'
Vue.use(Router)
export default new Router({
routes: [
// {
// path: '/',
// name: 'HelloWorld',
// component: HelloWorld
// }
{
path: '/',
name: 'HelloWorld',
component: () => import('@/components/HelloWorld.vue')
}
]
})
将注释的内容改为下面这种引入即可。详见之前写的这篇博客:vue-router 懒加载优化
10、ui框架按需加载
比如之前写的这篇博客:vue按需引入echarts
不多说,各ui框架均有介绍。
11、图片资源懒加载
对于图片过多的页面,为了加速页面加载速度,可以使用v-lazy之类的懒加载库或者绑定鼠标的scroll事件,滚动到可视区域先再对数据进行加载显示,减少系统加载的数据。这样对于页面加载性能上会有很大的提升,也提高了用户体验。
12、避免内存泄漏
关于内存泄漏,详见我之前总结的这2篇博客:
二、webpack打包优化
1、优化 SourceMap
source-map:一种提供源代码 到 构建后 代码映射技术(如果构建后的代码出错了,通过映射可以追踪源代码的错误)
打开webpack.config.js
source-map :外部,错误代码准确信息 和 源代码的错误位置
devtool的全部值及介绍
source-map: 一种 提供源代码到构建后代码映射 技术 (如果构建后代码出错了, 通过映射可以追踪源代码错误)
[inline-|hidden-|eval-] [nosources] [cheap-[module-]]source-map
source-map:外部--->错误代码准确信息, 源代码的错误位置
inline-source-map:内嵌--->错误代码准确信息 和源代码的错误位置
hidden-source-map:外部--->错误代码错误原因, 但没有错误位置,不能追踪源代码错误(隐藏源代码)
eval-source-map:内嵌--->错误代码准确信息, 源代码的错误位置
nosources-source-map:外联--->错误代码准确信息,但是没有任何源代码信息(隐藏源代码)
cheap-source-map:外部--->错误代码准确信息 和 源代码的错误位置,只能精确行
cheap-module-source-map外部--->错误代码准确信息, 源代码的错误位置
内联 和 外部的区别: 1. 外部生成了文件 , 内联没有文件, 2. 内联构建速度快
这么多source-map如何选择?
开发环境: 速度快,调试更友好
速度快( eval>inline>cheap>··· )
组合: eval-cheap-source-map > eval-source-map
调试更友好
组合source-map > cheap-module-source-map > cheap-source-map
最终结果:eval-source-map(速度快)和 cheap-module-source-map(性能更好) (vuecli与react脚手架默认)
生产环境: 源代码要不要隐藏?调试要不要更友好
内嵌会让代码体积变大,所以在生产环境下不用 内嵌
nosources-source-map 全部隐藏
hidden-source-map 只隐藏源代码,会提示构建后代码错误信息
最终结果:
source-map 和 cheap-module-source-map
开发环境: eval-source-map 或者 cheap-module-source-map
生产环境: source-map 或者 cheap-module-source-map
2、使用cdn引入第三方插件
打包时,把vue、vuex、vue-router、axios等,换用国内的bootcdn直接引入到根目录的index.html。在webpack设置中添加externals,忽略不需要打包的库。
module.exports = {
context: path.resolve(__dirname, '../'),
entry: {
app: './src/main.js'
},
externals:{
'vue':'Vue',
'vue-router':'VueRouter',
'vuex':'Vuex'
},
在index.html中使用cdn引入
<script src="//cdn.bootcss.com/vue/2.2.5/vue.min.js"></script>
<script src="//cdn.bootcss.com/vue-router/2.3.0/vue-router.min.js"></script>
<script src="//cdn.bootcss.com/vuex/2.2.1/vuex.min.js"></script>
<script src="//cdn.bootcss.com/axios/0.15.3/axios.min.js"></script>
去掉原有的引用,否则还是会打包
//去掉import,如:
//import Vue from 'vue'
//import Router from 'vue-router'
//去掉Vue.use(XXX),如:
//Vue.use(Router)
3、开启 gzip 压缩
安装 compression-webpack-plugin:cnpm i compression-webpack-plugin -D
在 vue.config.js中引入并修改 webpack配置:
const CompressionPlugin = require('compression-webpack-plugin')
configureWebpack: (config) => {
if (process.env.NODE_ENV === 'production') {
// 为生产环境修改配置...
config.mode = 'production'
return {
plugins: [new CompressionPlugin({
test: /\.js$|\.html$|\.css/, //匹配文件名
threshold: 10240, //对超过10k的数据进行压缩
deleteOriginalAssets: false //是否删除原文件
})]
}
}
}
在服务器我们也要做相应的配置,如果发送请求的浏览器支持 gzip,就发送给它 gzip 格式的文件,我的服务器是用 express框架搭建的,只要安装一下 compression就能使用
//注意,要放在所有其他中间件注册之前
const compression = require('compression')
app.use(compression())
更好的是直接通过nigix配置gzip压缩,详见之前我写的这篇博客:nginx配置解决vue单页面打包文件大,首次加载慢的问题
4、Webpack 对图片进行压缩
在 vue 项目中除了可以在 webpack.base.conf.js 中 url-loader 中设置 limit 大小来对图片处理,对小于 limit 的图片转化为 base64 格式,其余的不做操作。所以对有些较大的图片资源,在请求资源的时候,加载会很慢,我们可以用 image-webpack-loader来压缩图片:
首先,安装 image-webpack-loader:npm install image-webpack-loader --save-dev
然后,在 webpack.base.conf.js 中进行配置
{
test: /\.(png|jpeg|gif|svg)(\?.*)?$/,
use:[
{
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
loader: 'image-webpack-loader',
options: {
bypassOnDebug: true,
}
}
]
}
5、减少 ES6 转为 ES5 的冗余代码
Babel 插件会在将 ES6 代码转换成 ES5 代码时会注入一些辅助函数,例如下面的 ES6 代码:
class HelloWebpack extends Component{...}
//这段代码再被转换成能正常运行的 ES5 代码时需要以下两个辅助函数:
babel-runtime/helpers/createClass // 用于实现 class 语法
babel-runtime/helpers/inherits // 用于实现 extends 语法
在默认情况下, Babel 会在每个输出文件中内嵌这些依赖的辅助函数代码,如果多个源代码文件都依赖这些辅助函数,那么这些辅助函数的代码将会出现很多次,造成代码冗余。为了不让这些辅助函数的代码重复出现,可以在依赖它们时通过 require(‘babel-runtime/helpers/createClass’) 的方式导入,这样就能做到只让它们出现一次。babel-plugin-transform-runtime 插件就是用来实现这个作用的,将相关辅助函数进行替换成导入语句,从而减小 babel 编译出来的代码的文件大小。
首先,安装 babel-plugin-transform-runtime :npm install babel-plugin-transform-runtime --save-dev
然后,修改 .babelrc 配置文件为:
"plugins": [
"transform-runtime"
]
6、提取公共代码
如果项目中没有去将每个页面的第三方库和公共模块提取出来,则项目会存在以下问题:
1、相同的资源被重复加载,浪费用户的流量和服务器的成本。
1、每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验。
所以我们需要将多个页面的公共代码抽离成单独的文件,来优化以上问题 。Webpack 内置了专门用于提取多个Chunk 中的公共部分的插件 CommonsChunkPlugin,我们在项目中 CommonsChunkPlugin 的配置如下:
// 所有在 package.json 里面依赖的包,都会被打包进 vendor.js 这个文件中。
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function(module, count) {
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
);
}
}),
// 抽取出代码模块的映射关系
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
chunks: ['vendor']
})
7、模板预编译
当使用 DOM 内模板或 JavaScript 内的字符串模板时,模板会在运行时被编译为渲染函数。通常情况下这个过程已经足够快了,但对性能敏感的应用还是最好避免这种用法。
预编译模板最简单的方式就是使用单文件组件——相关的构建设置会自动把预编译处理好,所以构建好的代码已经包含了编译出来的渲染函数而不是原始的模板字符串。
如果你使用 webpack,并且喜欢分离 JavaScript 和模板文件,你可以使用 vue-template-loader,它也可以在构建过程中把模板文件转换成为 JavaScript 渲染函数。
8、提取组件的 CSS
当使用单文件组件时,组件内的 CSS 会以 style 标签的方式通过 JavaScript 动态注入。这有一些小小的运行时开销,如果你使用服务端渲染,这会导致一段 “无样式内容闪烁 (fouc) ” 。将所有组件的 CSS 提取到同一个文件可以避免这个问题,也会让 CSS 更好地进行压缩和缓存。
查阅这个构建工具各自的文档来了解更多:webpack + vue-loader ( vue-cli 的 webpack 模板已经预先配置好)、Browserify + vueify、Rollup + rollup-plugin-vue。
详见之前写的这篇博客:NuxtJS处理因css在服务端渲染而增加源代码量,从而影响到SEO的问题及VUE提取 CSS 到单个文件