最近接手了别人做的vue项目,项目跑起来后,有些页面很卡,首屏加载也慢,打包速度也慢。于是,研究了很久vue的项目性能优化,下面我将从两个部分来详解vue项目的性能优化:
代码优化webpack打包优化v-if 是懒加载,当状态为true时才会加载,并且为 false 时不会占用布局空间; v-show 是无论状态是 true 或者是 false,都会进行渲染,并且只是简单地基于 CSS 的 display 属性进行切换,并对布局占据空间对于在项目中,需要频繁调用,不需要权限的显示隐藏,可以选择使用 v-show,可以减少系统的切换开销。
v-for 在列表数据进行遍历渲染时,需要为每一项item设置唯一key值,方便vue.js内部机制精准找到该条列表数据。当state更新时,新的状态值和旧的状态值对比,较快地定位到diff。
当 v-if 与 v-for 一起使用时,v-for 具有比 v-if 更高的优先级,这意味着 v-if 将分别重复运行于每个 v-for 循环中。所以,不推荐v-if和v-for同时使用,必要情况下可以替换成 computed 属性。
computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值; watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;
运用场景: 1、当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算; 2、当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
Vue 会通过 Object.defineProperty 对数据进行劫持,来实现视图响应数据的变化,然而有些时候我们的组件就是纯粹的数据展示,不会有任何改变,我们就不需要 Vue 来劫持我们的数据,在大量数据展示的情况下,这能够很明显的减少组件初始化的时间,那如何禁止 Vue 劫持我们的数据呢?可以通过 Object.freeze 方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了。
//Object.freeze 方法冻结对象 this.data = Object.freeze(res.data);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的数据驱动视图更新比较慢,造成渲染比较慢。造成比较差的体验效果。所以把组件细分,比如一个组件,可以把整个组件细分成头部组件、左侧菜单组件、内容区组件等。能复用的功能一定要封装成公共组件,例如一些弹窗组件。这样,不仅加载速度更快,而且还更好维护。
这里以饿了么ui为例: 原本的引进方式引进了整个包:
import ElementUI from 'element-ui' Vue.use(ElementUI)但实际上我用到的组件只有按钮,分页,表格,输入与警告 所以我们要按需引用:
import { Button, Input, Pagination, Table, TableColumn, MessageBox } from 'element-ui'; Vue.use(Button) Vue.use(Input) Vue.use(Pagination) Vue.prototype.$alert = MessageBox.alert注意 MessageBox注册方法的区别,并且我们虽然用到了 alert,但并不需要引入 Alert组件 在 .babelrc / babel.config.js文件中添加( vue-cli 3要先安装 babel-plugin-component):
plugins: [ [ "component", { "libraryName": "element-ui", "styleLibraryName": "theme-chalk" } ] ]对于图片过多的页面,为了加速页面加载速度,可以使用v-lazy之类的懒加载库或者绑定鼠标的scroll事件,滚动到可视区域先再对数据进行加载显示,减少系统加载的数据。这样对于页面加载性能上会有很大的提升,也提高了用户体验。
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') } ] })source-map:一种提供源代码 到 构建后 代码映射技术(如果构建后的代码出错了,通过映射可以追踪源代码的错误)
打开webpack.config.js
source-map :外部,错误代码准确信息 和 源代码的错误位置 devtool的全部值
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
打包时,把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)安装 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())在 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|jpe?g|gif|svg)(\?.*)?$/, use:[ { loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('img/[name].[hash:7].[ext]') } }, { loader: 'image-webpack-loader', options: { bypassOnDebug: true, } } ] }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" ]如果项目中没有去将每个页面的第三方库和公共模块提取出来,则项目会存在以下问题:
相同的资源被重复加载,浪费用户的流量和服务器的成本。
每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验。
所以我们需要将多个页面的公共代码抽离成单独的文件,来优化以上问题 。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'] })当使用 DOM 内模板或 JavaScript 内的字符串模板时,模板会在运行时被编译为渲染函数。通常情况下这个过程已经足够快了,但对性能敏感的应用还是最好避免这种用法。
预编译模板最简单的方式就是使用单文件组件——相关的构建设置会自动把预编译处理好,所以构建好的代码已经包含了编译出来的渲染函数而不是原始的模板字符串。
如果你使用 webpack,并且喜欢分离 JavaScript 和模板文件,你可以使用 vue-template-loader,它也可以在构建过程中把模板文件转换成为 JavaScript 渲染函数。
当使用单文件组件时,组件内的 CSS 会以 style 标签的方式通过 JavaScript 动态注入。这有一些小小的运行时开销,如果你使用服务端渲染,这会导致一段 “无样式内容闪烁 (fouc) ” 。将所有组件的 CSS 提取到同一个文件可以避免这个问题,也会让 CSS 更好地进行压缩和缓存。
查阅这个构建工具各自的文档来了解更多:
webpack + vue-loader ( vue-cli 的 webpack 模板已经预先配置好) Browserify + vueify Rollup + rollup-plugin-vue