Vue 性能优化实践指北

1,178 阅读7分钟

说在最前面的

因为最近在来到了新的公司,在写新的项目,前端技术栈是vue,就打起了vue的性能优化的主意。
本文因为都是自己个人的一些总结的原因,所以有些地方可能难免会有些不足,还望各位大佬可以指出,我好作更改~

本文将会长期更新~

图片优化

更改下思路,现在认为图片的优化在一个项目起到的作用是最大的,例如将一个原本有着1M或者五六百k的图片优化一下,包括但不限于使用CDN、压缩文件等。 这部分的优化不局限于在vue中使用,几乎在所有的前端项目中都是通用的~

打包优化

路由懒加载

{
   path: '/home',
   name: 'home',
   component: import(/* webpackChunkName: '你要设置的chunk的name' */ '../page/home')
}

组件懒加载

这里的 /* webpackChunkName: "[request]" */ 中的 request 表示实际解析的文件名。

const Tabs = () => import(/* webpackChunkName: "[request]" */'_c/Tabs')

export default {
    components: {
        IconList
    }
}

第三方CDN

2020-10-3 首先,为什么我们要明确我们为什么要使用externals~
我们在进行开发的时候,我们有可能会使用splitChunks来进行代码的分割,但是对于一些存在着官方CDN的外部库,我们还是更期望使用CDN。
webpack已经为我们提供了externals属性,一些平时基本不会改变的基础库我们可以将它们独立出来单独加载。如此,我们可以就能利于浏览器的缓存机制,不用每次都重新加载这些库文件。 我推荐如下写法:

//vue.config.js
// chainWebpack中,判断是生产环境时
	config.externals({	//这里需要注意,chainWebpack是链式调用,所以这边是一个函数,不然我们的包不会被排除
            vue: 'Vue',
            'element-ui': 'ELEMENT',
            'vue-router': 'VueRouter',
            vuex: 'Vuex',
            axios: 'axios',
            'echarts': 'echarts'
          })
          const cdn = {
            css: ['//unpkg.com/element-ui@2.13.2/lib/theme-chalk/index.css'],
            js: [
              '//unpkg.com/vue@2.6.10/dist/vue.min.js',
              '//unpkg.com/vue-router@3.0.2/dist/vue-router.min.js',
              '//unpkg.com/vuex@3.1.0/dist/vuex.min.js',
              '//unpkg.com/axios@0.18.1/dist/axios.min.js',
              '//unpkg.com/element-ui@2.13.2/lib/index.js',
              'https://cdn.bootcdn.net/ajax/libs/echarts/4.2.1/echarts.min.js'
            ]
          }
          config.plugin('html').tap(args => {
            args[0].cdn = cdn
            return args
          })

到这里之后,我们不要忘记在index.html中添加cdn的引用~

<!-- 使用CDN的CSS文件 -->
<% for (var i in htmlWebpackPlugin.options.cdn &&
htmlWebpackPlugin.options.cdn.css) { %>
  <link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" />
  <% } %>

  <!-- 使用CDN的JS文件 -->
  <% for (var i in htmlWebpackPlugin.options.cdn &&
htmlWebpackPlugin.options.cdn.js) { %>
  <script type="text/javascript" src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
  <% } %>

当然了,如果你不喜欢这种写法,我们还以使用一种更加朴素的写法~

<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css" /> 
<script src="https://unpkg.com/element-ui/lib/index.js"></script>

在这里的CDN,我是更加推荐使用官方的CND,这样相对来说更稳定一些~ 当然了,这只是我个人的一点建议~

**TIPS: ** 对于静态资源,对于字体文件,推荐使用CDN,或者如果你的产品其中的文字基本都是一些固定一变的,可以学习一下苹果官网对于字体的处理方式~

splitChunk

2020-10-3
对于splitChunk,目前已经可以搜索到很多的使用方式了 首先,虽然我们都说它的最重要的配置项是cacheGroups,但是其它的我们也需要去了解,有的时候如果不解的话,就有可能会踩坑

  1. chunks:表示从哪些chunks里面抽取代码,除了三个可选字符串值 initial、async、all 之外,还可以通过函数来过滤所需的 chunks;
  2. minSize:表示抽取出来的文件在压缩前的最小大小,默认为 30000;
  3. maxSize:表示抽取出来的文件在压缩前的最大大小,默认为 0,表示不限制最大大小;
  4. minChunks:表示被引用次数,默认为1;
  5. maxAsyncRequests:最大的按需(异步)加载次数,默认为 5;
  6. maxInitialRequests:最大的初始化加载次数,默认为 3;
  7. automaticNameDelimiter:抽取出来的文件的自动生成名字的分割符,默认为 ~;
  8. name:抽取出来文件的名字,默认为 true,表示自动生成文件名;
  9. cacheGroups: 缓存组 说一个我自己踩过的坑,可以看看我下面的这段代码:
	//vue.config.js => chainWebpack(config)
		config
            .optimization.splitChunks({
              chunks: 'all',
              cacheGroups: {
                libs: {
                  name: 'chunk-libs',
                  test: /[\\/]node_modules[\\/]/,
                  priority: 10,
                  chunks: 'initial' 
                },
                vexTable: {
                  name: 'chunk-vexTable', 
                  priority: 30,
                  test: /[\\/]node_modules[\\/]_?vxe-table(.*)/ 
                },
                moment: {
                  name: 'chunk-moment', 
                  priority: 30, 
                  test: /[\\/]node_modules[\\/]_?moment(.*)/ 
                },
                commons: {
                  name: 'chunk-commons',
                  test: resolve('src/components'), 
                  minChunks: 1, 
                  priority: 5,
                  reuseExistingChunk: true
                }
              }
            })
          

当燃了,我之前没有看完api,所以我理所当然的认为,我这样做是完全可行的,但是当我打包之后我发现永远只能生成三个chunk

那么,对于这个问题,需要怎么解决呢?
我自己的解决方法是这样的:

  1. Element使用CDN,echarts按需加载
  2. maxAsyncRequests 和 maxInitialRequests 两个配置项调整了一下

Image-webpack-loader

Image-webpack-loader 可以在打包的时候对图片进行优化

chainWebpack(config) {
  /** 忽略其它配置 ***/
  config
      .when(process.env.NODE_ENV !== 'development',
   /** 忽略其它配置 ***/
        config => {
          config.module
            .rule('images')
            .use('image-webpack-loader')
            .loader('image-webpack-loader')
            .options({
              mozjpeg: { progressive: true, quality: 65 },
              optipng: { enabled: false },
              pngquant: { quality: [0.65, 0.9], speed: 4 },
              gifsicle: { interlaced: false }
              // webp: { quality: 75 }
            })
    /** 忽略其它配置 ***/
        })
}

这边,我写在这个的时候遇到了如下问题:

  1. image-webpack-plugin无法安装的问题

解决方案:这边使用cnpm进行安装

2. 在打完压缩完之后,误以为图无用,需要注意,这边Png的图片,可能会用默认的预览无法查看,但是在浏览器上应该是可以查看的。 2. libpng的问题,因为这是mac电脑,我用homebrew安装过了,不清楚win中的问题

ScriptExtHtmlWebpackPlugin

这个插件是会将我拉runtimechunck之后的runtimeChunk.xx.js这样的文件内联至我们的Index.js文件之中。因为这样的一个文件是经常改变,如果不作处理的话,我们每次都需要重新请求它,如果将之进行内联的话,可以大大节省我们的时间。
配置代码如下:

					config
            .plugin('ScriptExtHtmlWebpackPlugin')
            .after('html')
            .use('script-ext-html-webpack-plugin', [{
              inline: /runtime\..*\.js$/
            }])
            .end()

这段代码也是在生产打包的时候才应该执行的,所以我将之放在 chainWebpack 中,这里有一点需要说明的是它最好在 splitChunk 之前,同时如果你使用了 html-webpack-plugin 的话,那么它也必须要是在 html-webpack-plugin 之后,注意是必须!

terser-webpack-plugin

在这里,我最初的选择是 uglifyjs-webpack-plugin ,这里之所以不使用它,因为它不支持es6的语法,总是会报一些奇奇怪怪的错误。然后我选选择安装和使用 terser-webpack-plugin 
注意,这里里我发现了一个问题,通过查阅vue-cli4的升级变化,我发现了vue-cli4已经为我们集成了这个插件,我们只需要在webpckChain里对它进行操作就行了,具体的代码使用如下:

			config.optimization.minimizer('terser').tap((args) => {
            // 去除生产环境console
            args[0].terserOptions.compress.drop_console = true
            return args
          })

我选择了在生产环境时去除我们的console

gzip

对于gzip这个并没有什么好说的,我在这里选择使用的是 compression-webpack-plugin ,以下是我的具体配置

		config.plugin('CompressionWebpackPlugin')
            .use('compression-webpack-plugin', [{
              filename(asset) { asset = '[file].gz[query]'; return asset },
              algorithm: 'gzip',
              test: productionGzipExtensions,
              threshold: 10240, // 只有大小大于该值的资源会被处理 10240
              minRatio: 0.8, // 只有压缩率小于这个值的资源才会被处理
              deleteOriginalAssets: false // 删除原文件
            }])

可能在写法上会和你在网上看到的有所不同,但是相信我,它们的效果应该是一样的。

热更新失效

对于偶然的热更新失效的问题,这里网上也有提供许多的方法,有一些是通过安装webpack-dev-server来解决这样的问题,这里,我推荐使用如下方法:

config.resolve.symlinks(true);

代码优化

前文在对于vue-cli4的一优化已经讲了许多,接下来我想说说关于代码层面的优化。
作为一个程序员,我们应该享受 code review 的过程~

v-if 与 v-show

首先明确二者的区别

  1. v-if 如果为false,则初始不会渲染,如果切换v-if的值,它总是会重新渲染
  2. v-show 不管初始值为何,都会渲染,且切换值仅仅是切换display:none这一css

在一些特定的场景下,如果某一个组件会被频繁的显示与隐藏,那么这里更推荐使用 v-show 

那么,如果我们的这个组件每次我们都想要将之重新渲染,那么我们可以在这里使用v-if来触发它的强制重新渲染

在这里,如果你频繁地需要通过v-if来进行强制重新渲染的话,那么这是vue官方不建议的

vue为我们推荐了两种强制渲染视图的方法:

forceUpdate

使用这个方法的时候,我们需要注意的是,这个方法不会更新我们的计算属性,它仅仅会强制渲染我们的视图
我们需要这么做:


//component.vue
export default {
	methods:{
    componentUpdate(){
      this.$forceUpdate()
    }
  }
}

key的方式

我们可以这么使用

//component.vue
<template>
  <component-child :key='componentChildKey'></component-child>
</template>
<script>
  export default {
  	data(){
    	return {
      	componentChildKey:1
      }
    },
    methods:{
      upDateComponentChild(){
      	this.componentChildKey += 1
      }
    }
  }
</script>

善用template

对于一些不必要的外层元素,我们总是应该尽可能地使用template来替代它

v-for 与 v-if 与 key

首先,我们必须要明确,v-for与v-if不能是同级的,同时,我们使用了v-for应该为循环荐加上一个key,以提高diff的运行效率。
同时,如果我们的v-for 放在template上,那么,我们的:key不能放在template上,应该放在被包裹的最外层元素上。

而至于,为什么会提高diff的效率,将会在以后补充~

v-once

对于仅会渲染一次的元素,我们应该使用v-once
这里需要注意,如果我们在v-for中使用了v-once,那么我们必须要为这个使用的元素加上一个key

它的原理,将会在以后进行补充~

数据过滤的必要

Vue在初始化的时候会对数据进行劫持,为数据对象的每个属性添加依赖
这里,我推荐使用map映射的方式来解决这个问题,即是在获取的时候就通过map将数据“洗”一遍,剔除一些我们并不需要的数据项。

同样地数据处理问题

对于完全不需要进行响应式变动的数据,我们可以使用Object.freeze()来将之进行冻结,对于configurable属性为false的数据,vue在劫持的时候会跳过~

按需引入

类似element的需要引入这种

v-lazyload的使用

这是一个vue的懒加载的使用

对于异步请求

这里,我们如果使用了异步请求接到了一个Promise数据,我推荐使用async await的方法,并且最好是使用try catch进行包裹

对于在actions中发起请求,我们建议是如果这里不会触发state或者mutations的操作,就尽量不要将请求放到actions中。

对于watch computed 以及metheds的合理使用

  1. watch

监听某一个数据,它会影响到多个数据或者当它变化时,我们需要进行大量的其它操作

  1. computed

它是依赖于其它数据进行变化的,亦或者此项数据的计算有着极大地性能消耗

  1. methods

它是实时更新的

同时监听多个值

如果一个数据的变化,同时依赖于多个数据,我们最好是使用以下这种方法

<script>
export default(){
	computed:{
    handelChange(){
      const {fileName,FolderName} = this.$props
    }
	},
    watch:{
    	handelChange:{
      	handler:function(val,oldval){
          //do somting
        },
        immediate:true
      }
    }
}
</script>

对于图片

我个人建议对于大图片使用Picture标签的source

其它优化

SEO优化

这里不仅仅是对SEO的优化,还会包含对于首屏白屏的一些优化

预渲染

后续完善

SSR

后续完善