前言:vue-cli项目开发打包部署后,存在问题有首次首页加载过慢,包括加载缓慢问题,需要进行vue项目优化。下面是对vue性能优化方法进行归纳,后面会对方法进行亲测。
主要包括:代码包打包优化、编码优化、用户体验优化
一、代码包打包优化
可以在谷歌浏览器的调试工具(F12)中看到打包后生成的app.js文件过大;
1、屏蔽sourceMap
进行打包源码上线环节,需要对项目开发环节的开发提示信息以及错误信息进行屏蔽,一方面可以减少上线代码包的大小;另一方面提高系统的安全性。在vuejs项目的config目录下有三个文件dev.env.js(开发环境配置文件)、prod.env.js(上线配置文件)、index.js(通用配置文件)。vue-cli脚手架在上线配置文件会自动设置允许sourceMap打包,所以在上线前可以屏蔽sourceMap。如下所示,index.js的配置如下,通用配置文件分别对开发环境和上线环境做了打包配置分类,在build对象中的配置信息中,productionSourceMap修改成false:
module.exports = {dev: {...},build: {// Template for index.htmlindex: path.resolve(__dirname, '../dist/ndindex.html'),// PathsassetsRoot: path.resolve(__dirname, '../dist'),assetsSubDirectory: 'static',assetsPublicPath: './',/*** Source Maps*/productionSourceMap: false, //这里关闭Source Maps// https://webpack.js.org/configuration/devtool/#productiondevtool: '#source-map',// Gzip off by default as many popular static hosts such as// Surge or Netlify already gzip all static assets for you.// Before setting to `true`, make sure to:// npm install --save-dev compression-webpack-pluginproductionGzip: true,productionGzipExtensions: ['js', 'css','svg'],// Run the build command with an extra argument to// View the bundle analyzer report after build finishes:// `npm run build --report`// Set to `true` or `false` to always turn it on or offbundleAnalyzerReport: process.env.npm_config_report}
}
2、对项目代码中的JS/CSS/SVG(*.ico)文件进行gzip压缩
在vue-cli脚手架的配置信息中,有对代码进行压缩的配置项,例如index.js的通用配置,productionGzip设置为true,但是首先需要对compress-webpack-plugin支持,所以需要通过 npm install --save-dev compression-webpack-plugin,gzip会对js、css文件进行压缩处理;对于图片进行压缩问题,对于png,jpg,jpeg没有压缩效果,对于svg,ico文件以及bmp文件压缩效果达到50%,在productionGzipExtensions: ['js', 'css','svg']设置需要进行压缩的什么格式的文件。对项目文件进行压缩之后,需要浏览器客户端支持gzip以及后端支持gzip。下面可以查看成功支持gzip状态:
module.exports = {dev: {...},build: {...// Gzip off by default as many popular static hosts such as// Surge or Netlify already gzip all static assets for you.// Before setting to `true`, make sure to:// npm install --save-dev compression-webpack-pluginproductionGzip: true,productionGzipExtensions: ['js', 'css','svg'],...}
}
二、源码优化
1、对路由组件进行懒加载
懒加载也叫延迟加载,即在需要的时候进行加载,随用随载。
在路由配置文件里,这里是router.js里面引用组件。如果使用同步的方式加载组件,在首屏加载时会对网络资源加载加载比较多,资源比较大,加载速度比较慢。所以设置路由懒加载,按需加载会加速首屏渲染。在没有对路由进行懒加载时,在Chrome里devtool查阅可以看到首屏网络资源加载情况(6requests 3.8MB transfferred Finish:4.67s DOMContentLoaded 2.61s Load 2.70s)。在对路由进行懒加载之后(7requests 800kb transffered Finish2.67s DOMContentLoaded 1.72s Load 800ms),可以看见加载速度明显加快。但是进行懒加载之后,实现按需加载,那么项目打包不会把所有js打包进app.[hash].js里面,优点是可以减少app.[hash].js体积,缺点就是会把其它js分开打包,造成多个js文件,会有多次https请求。如果项目比较大,需要注意懒加载的效果
使用如下:
routes: [{ path: "/", redirect: "index" },{path: "/",name: "home",component: resolve=>require(["@/views/home"],resolve),children: [{// 员工查询path: "/employees",component: resolve=>require(["@/components/employees"],resolve)},{// 首页path: "/index",component: resolve=>require(["@/views/index"],rolve)},]}
]
2、组件异步加载
vue官网指南,提到异步组件的使用,在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。Vue 只有在这个组件需要被渲染的时候才会触发工厂函数,且会把结果缓存起来供未来重渲染。
2.1、v-if惰性结合setTimeout 使组件异步加载
加载首页的时候,可以先给首页的子组件设置v-if = “false”,在页面初始化的时候再给子组件设置为true,此方法利用了v-if的惰性,setTimeout会使子组件在所有的组件初始化完成并显示后再对其子组件进行初始化。
注:在实际开发中还遇到了另一种情况也可以用此方法解决,在入口js中获取了app的token,但是在具体页面中发现不管是在created还是mounted中都是有时候能获取到token,有时候又不可以,是因为执行顺序的原因,可以通过 setTimeout 时间设置为0 这种方法把用到token的请求方法给排到最后,这样就能保证请求方法中有token了。
2.2、异步组件,按需加载
webpack 2 结合 ES2015 语法如下。
Vue.component('async-webpack-example',// 这个 `import` 函数会返回一个 `Promise` 对象。() => import('./my-async-component')
)
上面全局组件,当使用局部注册的时候如下,因为import函数返回的是一个promise对象,因此可以用promise本身的then()和catch()方法去监听到组件的加载。
new Vue({// ...components: {'my-component': () => import('./my-async-component')}
})
3、vue-lazyload插件,图片懒加载
项目中过多的图片会严重影响网页的加载速度,并且移动网络下的流量消耗巨大,所以说延迟加载几乎是标配了。 图片懒加载,显示当前用户界面再加载显示图片,实现的原理很简单,就是我们先设置图片的data-set属性(当然也可以是其他任意的,只要不会发送http请求就行了,作用就是为了存取值)值为其图片路径,由于不是src,所以不会发送http请求。 然后我们计算出页面scrollTop的高度和浏览器的高度之和, 如果图片举例页面顶端的坐标Y(相对于整个页面,而不是浏览器窗口)小于前两者之和,就说明图片就要显示出来了(合适的时机,当然也可以是其他情况),这时候我们再将 data-set 属性替换为 src 属性即可。
3.1、JavaScript实现:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Lazyload 2</title><style>img {display: block;margin-bottom: 50px;height: 200px;}</style>
</head>
<body><img src="images/loading.gif" data-src="images/1.png"><img src="images/loading.gif" data-src="images/2.png"><img src="images/loading.gif" data-src="images/3.png"><img src="images/loading.gif" data-src="images/4.png"><img src="images/loading.gif" data-src="images/5.png"><img src="images/loading.gif" data-src="images/6.png"><script>function throttle(fn, delay, atleast) {//函数绑定在 scroll 事件上,当页面滚动时,避免函数被高频触发,var timeout = null,//进行去抖处理startTime = new Date();return function() {var curTime = new Date();clearTimeout(timeout);if(curTime - startTime >= atleast) {fn();startTime = curTime;}else {timeout = setTimeout(fn, delay);}}}function lazyload() {var images = document.getElementsByTagName('img');var len = images.length;var n = 0; //存储图片加载到的位置,避免每次都从第一张图片开始遍历 return function() {var seeHeight = document.documentElement.clientHeight;var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;for(var i = n; i < len; i++) {if(images[i].offsetTop < seeHeight + scrollTop) {if(images[i].getAttribute('src') === 'images/loading.gif') {images[i].src = images[i].getAttribute('data-src');}n = n + 1;}}}}var loadImages = lazyload();loadImages(); //初始化首页的页面图片window.addEventListener('scroll', throttle(loadImages, 500, 1000), false);//函数节流(throttle)与函数去抖(debounce)处理,
//500ms 的延迟,和 1000ms 的间隔,当超过 1000ms 未触发该函数,则立即执行该函数,不然则延迟 500ms 执行该函数</script>
</body>
</html>
3.2、vue-cli项目中vue-lazyload 插件实现
3.2.1. 安装插件:
npm install vue-lazyload --save-dev
3.2.2. main.js引入插件:
import VueLazyLoad from 'vue-lazyload'
Vue.use(VueLazyLoad,{error:'./static/error.png',loading:'./static/loading.png'
})
3.2.3. vue文件中将需要懒加载的图片绑定 v-bind:src 修改为 v-lazy
<img class="item-pic" v-lazy="newItem.picUrl"/>
功能扩展:
图片懒加载的简单效果已经实现了,然后就可以按这开发文档的api进行扩展了:
key | description | default | options |
---|---|---|---|
preLoad | proportion of pre-loading height(预加载高度比例) | 1.3 | Number |
error | src of the image upon load fail(图片路径错误时加载图片) | 'data-src' | String |
loading | src of the image while loading(预加载图片) | 'data-src' | String |
attempt | attempts count(尝试加载图片数量) | 3 | Number |
listenEvents | events that you want vue listen for (想要监听的vue事件) 默认['scroll']可以省略, 当插件跟页面中的动画或过渡等事件有冲突是, 可以尝试其他选项 |
| Desired Listen Events |
adapter | dynamically modify the attribute of element (动态修改元素属性) | { } | Element Adapter |
filter | the image's listener filter(动态修改图片地址路径) | { } | Image listener filter |
lazyComponent | lazyload component | false | Lazy Component |
dispatchEvent | trigger the dom event | false | Boolean |
throttleWait | throttle wait | 200 | Number |
observer | use IntersectionObserver | false | Boolean |
observerOptions | IntersectionObserver options | { rootMargin: '0px', threshold: 0.1 } | IntersectionObserver |
4、引入外部插件或CDN引用,不要在vue中引入
我们可以打包 时不打包 vue、vuex、vue-router、axios 等,换用国内的 bootcdn 直接引入到根目录的 index.html 中,这样可以减少app.js大小。采用CDN外部加载,去掉其他页面的组件import,修改webpack.base.config.js,在externals中加入该组件,这是为了避免编译时找不到组件报错。
<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>
externals: {'vue': 'Vue','vue-router': 'VueRouter','vuex': 'Vuex','axios': 'axios'
}
5、使用到第三方库的时按需引用
在项目开发中,我们会用到很多第三方库,如果可以按需引入,我们可以只引入自己需要的组件,来减少组件库所占空间,如element-ui组件库按需只加载部分组件Button、Select,官网 按需引入element-ui
5.1. 安装babel-plugin-component插件:
npm install babel-plugin-component -D
5.2. 配置插件,将 .babelrc修改为:
{"presets": [["es2015", { "modules": false }]],"plugins": [["component",{"libraryName": "element-ui","styleLibraryName": "theme-chalk"}]]
}
5.3. 引入部分组件,比如 Button 和 Select,那么需要在 main.js 中写入以下内容:
import Vue from 'vue';
import { Button, Select } from 'element-ui';
import App from './App.vue';Vue.component(Button.name, Button);
Vue.component(Select.name, Select);
/* 或写为* Vue.use(Button)* Vue.use(Select)*/new Vue({el: '#app',render: h => h(App)
});
三、用户体验优化
1、loading加载效果
用于加载数据时显示动效。当请求服务端接口需要一定时间时,在请求时加上一个loading 加载动画效果将极大提升用户体验和减轻服务端压力。
实现方案:
a、使用elementUI的loading组件,可以通过指令或服务的形式调用。
指令形式调用:可以自定义加载动画的文字、遮罩层颜色、spinner加载图标的类名,如下:
<template><el-tablev-loading="loading"element-loading-text="拼命加载中"element-loading-spinner="el-icon-loading"element-loading-background="rgba(0, 0, 0, 0.8)":data="tableData"style="width: 100%"><el-table-columnprop="date"label="日期"width="180"></el-table-column><el-table-columnprop="name"label="姓名"width="180"></el-table-column><el-table-columnprop="address"label="地址"></el-table-column></el-table>
</template><script>export default {data() {return {tableData: [{date: '2016-05-03',name: '王小虎',address: '上海市普陀区金沙江路 1518 弄'}, {date: '2016-05-02',name: '王小虎',address: '上海市普陀区金沙江路 1518 弄'}, {date: '2016-05-04',name: '王小虎',address: '上海市普陀区金沙江路 1518 弄'}],loading: true};}};
</script>
上述loading布尔值可以结合vuex状态进行全局控制是否展示loading效果;
引入 Loading 服务:
import { Loading } from 'element-ui';
在需要调用时:
Loading.service(options);
其中 options
参数为 Loading 的配置项,具体见下表。LoadingService
会返回一个 Loading 实例,可通过调用该实例的 close
方法来关闭它:
let loadingInstance = Loading.service(options);
this.$nextTick(() => { // 以服务的方式调用的 Loading 需要异步关闭loadingInstance.close();
});
需要注意的是,以服务的方式调用的全屏 Loading 是单例的:若在前一个全屏 Loading 关闭前再次调用全屏 Loading,并不会创建一个新的 Loading 实例,而是返回现有全屏 Loading 的实例:
let loadingInstance1 = Loading.service({ fullscreen: true });
let loadingInstance2 = Loading.service({ fullscreen: true });
console.log(loadingInstance1 === loadingInstance2); // true
此时调用它们中任意一个的 close
方法都能关闭这个全屏 Loading。
如果完整引入了 Element,那么 Vue.prototype 上会有一个全局方法 $loading
,它的调用方式为:this.$loading(options)
,同样会返回一个 Loading 实例
b、可以使用命令【npm install --save vue-element-loading】安装该插件后直接使用
使用:
import Vue from 'vue'import VueElementLoading from 'vue-element-loading'Vue.component('VueElementLoading', ElementLoading)
Or
import VueElementLoading from 'vue-element-loading'export default {components: {VueElementLoading}}
//全屏
<vue-element-loading :active="isActive" :is-full-screen="true"/>//组件内容器
<div class="my-container"><vue-element-loading :active="isActive" spinner="bar-fade-scale" color="#FF6700"/><span>Lorem ipsum dolor sit amet, consectetur adipiscing elit.Fusce id fermentum quam. Proin sagittis, nibh id hendrerit imperdiet, elit sapien laoreet elit.</span>
</div>
Options
Props | Type | Default | Description |
---|---|---|---|
active | Boolean | - | Status for show/hide loading |
spinner | String | spinner | Spinner icon name: spinner, mini-spinner, ring, line-wave, line-scale, line-down, bar-fade, bar-fade-scale |
color | String | #ccc | Color of spinner icon |
is-full-screen | Boolean | false | Loader will overlay the full page |
background-color | String | rgba(255, 255, 255, .9) | Background color of spinner icon (for overlay) |
size | String | 40 | The size to display the spinner in pixels (NOTE: this will not affect custom spinner images) |
duration | String | 0.6 | The duration of one 'loop' of the spinner animation, in seconds (NOTE: this will not affect custom spinner images) |
text | String | - | Text will appear below loader |
text-style | Object | {} | Change style of the text below loader |
使用参考网址:https://biigpongsatorn.github.io/#/vue-element-loading可以看到有不同的loading动画效果,如下图
2、骨架屏加载
背景:使用Vue和Webpack进行**MPA(单、多页面应用)**的开发,一般会在页面进行数据接口等待时增加Loading动画,为用户提供较好的交互体验。但是会发现,Loading展示的时机是在Vue框架解析后,也就是说需要如下几个条件才能显示:
- HTML文件加载完成
- JavaScript文件加载完成
- window对象中完成webpackJsonp方法的添加
因此:等待的时间=HTML加载时间+JS加载时间+JS全局环境创建的执行时间。若是JS资源文件较大,或者存在过多的图片资源,导致资源速度下载较慢时,用户所能看到的白屏时间便较长。
因此添加骨架屏,其优势在于:
- 写于HTML文件中,独立于Vue框架,节省了JS加载时间+JS全局环境创建的执行时间的时间
- 只在主页面根据页面结构独立编写,预先展示页面结构,进行视觉暂留,提供更好的交互感官
- 只在页面结构变化时进行修改,维护成本相对较低
骨架屏的作用主要是在网络请求较慢时,提供基础占位,当数据加载完成,恢复数据展示。这样给用户一种很自然的过渡,不会造成页面长时间白屏或者闪烁等情况。 常见的骨架屏实现方案有ssr
服务端渲染和prerender
两种解决方案。
详细使用见本人文章:(亲测)vue-cli项目添加骨架屏多种方式,自动生成骨架屏
PS:
一些开发经验或习惯可以参考文章:https://blog.csdn.net/crazywoniu/article/details/73480344
可以学习的有:
- v-show,v-if 选择哪个?
v-if
,因为减少了 dom 数量,加快首屏渲染,v-if是懒加载,当状态为true时才会加载,并且为false时不会占用布局空间;v-show是无论状态是true或者是false,都会进行渲染,并对布局占据空间对于在项目中,需要频繁调用,不需要权限的显示隐藏,可以选择使用v-show,可以减少系统的切换开销。。 - 尽量不在模板里面写过多的表达式与判断
v-if="isShow && isAdmin && (a || b)"
,这种表达式虽说可以识别,但是不是长久之计,当看着不舒服时,适当的写到 methods 和 computed 里面封装成一个方法,这样的好处是方便我们在多处判断相同的表达式,其他权限相同的元素再判断展示的时候调用同一个方法即可。 - 循环调用子组件时添加 key,如
(item, index) in arr
,然后:key="index"
来确保 key 的唯一性。在列表数据进行遍历渲染时,需要为每一项item设置唯一key值,方便vuejs内部机制精准找到该条列表数据。当state更新时,新的状态值和旧的状态值对比,较快地定位到diff。 - 组件内样式命名尽量采用简短的命名规则,不需要
.header-title__text
之类的 class,直接.title
搞定。 - 全局的样式文件,尽量抽象化,既然不在每一个组件里重复写,就尽量通用,这部分抽象做的越好说明你的样式文件体积越小,复用率越高。建议将复写组件库如 Element 样式的代码也放到全局中去。
- 尽量不使用 float 布局,之前看到很多人封装了
.fl -- float: left
到全局文件里去,然后又要.clear
,现在的浏览器还不至于弱到非要用float
去兼容,完全可以 flex,grid 兼容性一般,功能其实 flex 布局都可以实现,float 会带来布局上的麻烦,用过的都知道不相信解释坑了。 - 尽量保持每个组件
export default {}
内的方法顺序一致,方便查找对应的方法。我个人习惯 data、props、钩子、watch、computed、components。 - props 父子组件传值时尽量
:width="" :heigth=""
不要:option={}
,细化的好处是只传需要修改的参数,在子组件 props 里加数据类型,是否必传,以及默认值,便于排查错误,让传值更严谨 - watch 和 computed 用哪个?,计算属性主要是做一层 filter 转换,切忌加一些调用方法进去,watch 的作用就是监听数据变化去改变数据或触发事件如
this.$store.dispatch('update', { ... })
- computed 中不能依赖一个计算结果去计算另一个计算值,因为computed中计算值是异步的,可能会报错undefined;。当watch的数据比较小,性能消耗不明显。当数据变大,系统会出现卡顿,所以减少watch的数据。
- 组件分类,我习惯性的按照三类划分,page、page-item 和 layout,page 是路由控制的部分,page-item 属于 page 里各个布局块如 banner、side 等等,layout 里放置多个页面至少出现两次的组件,如 icon, scrollTop 等,组件尽量实现 "高内聚低耦合";
- vuex状态过大时可以使用官网提供的模块化方案,vuex使用建议:尽量跑完完整的闭环是 store.dispatch('action') -> action -> commit -> mutation -> getter -> computed,为方便后期管理,在我的组件里只出现 dispatch 和 mapGetters,其余的流程都在名为 store 的 vuex 文件夹里进行。
- SSR(服务端渲染):如果项目比较大,首屏无论怎么做优化,都出现闪屏或者一阵黑屏的情况。可以考虑使用SSR(服务端渲染),vuejs官方文档提供next.js很好的服务端解决方案,但是局限性就是目前仅支持Koa、express等Nodejs的后台框架,需要webpack支持。目前自己了解的就是后端支持方面,vuejs的后端渲染支持php,其它的不太清楚。
参考文章:
浅谈 Vue 项目优化:https://blog.csdn.net/crazywoniu/article/details/73480344
vuejs项目性能优化总结:https://www.jianshu.com/p/41075f1f5297
关于vue在app首次加载缓慢的解决办法:https://www.jianshu.com/p/6262772bdc9c
图片懒加载和预加载:https://www.cnblogs.com/rlann/p/7296660.html
vue-lazyload 使用:https://www.cnblogs.com/xyyt/p/7650539.html
vue骨架屏官网:https://github.com/lavas-project/vue-skeleton-webpack-plugin
vue-element-loading: https://biigpongsatorn.github.io/#/vue-element-loading