9

随着互联网的发展,越来越多的公司都在使用Vue但是随着项目的越来越大,难免的会带来一系列的性能问题,笔者也为了这些问题而感到头疼,也同样的针对Vue的性能优化进行学习,已便在项目之出把性能问题规避掉,避免没有必要的返工。

为了方便以后能够快速的找到相关学习内容,在这里做一下记录,方便以后查看,同时也想把这些内容总结一下希望能够帮助更多的小伙伴一起学习,一起成长。Fighting~

这个时候可能会有很多小伙伴说,现在Vue3.0都快发布了为什么还要优化2.0的项目?因为公司80%的项目全是Vue2.0的项目,迁移的话成本太高,所以只能进行性能的优化调整。废话就不多赘述了,直接开始吧。

活用异步组件

Vue-cli打包的时候会把所有依赖的文件打包成一个很大的一个js文件中,当用户浏览网页的时候需要把整个js文件拉取过来,这样会导致页面在初始化的时候,页面会出现长时间的白屏情况,这个问题确实是蛮棘手的。

设想一下如果在页面中有很多的功能点,每个功能点又对应着不同的功能弹窗或者表单,首先第一点页面中的功能很多,我们不知道用户想要使用哪个功能,需要弹出哪个弹窗或表单,如果异步组件的情况,Vue-cli在打包的时候会把异步组件单独打包成一个文件,当用户使用的才会去加载这个js文件内容,这样无论是首屏的渲染起到了一定的优化的作用。

看下官网对于异步组件的说明:

在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。

//  代码截取自Vue官网
Vue.component('async-webpack-example',
  () => import('./my-async-component')
)

Vue官方为了解决组件加载时的等待过长,提供了异步组件加载Loading的异步组件:

//  代码截取自Vue官网
const AsyncComponent = () => ({
  // 需要加载的组件 (应该是一个 `Promise` 对象)
  component: import('./MyComponent.vue'),
  // 异步组件加载时使用的组件
  loading: LoadingComponent,
  // 加载失败时使用的组件
  error: ErrorComponent,
  // 展示加载时组件的延时时间。默认值是 200 (毫秒)
  delay: 200,
  // 如果提供了超时时间且组件加载也超时了,
  // 则使用加载失败时使用的组件。默认值是:`Infinity`
  timeout: 3000
})

使用异步组件需要注意以下几点:

  1. 如果只是需要异步的加载一些组件,先加载一部分,再加载一部分,那么就可以直接使用Vue官网的那种写法直接使用setTimeOut也不是未尝不可的。
  2. 如果是点击加载的话,一定要写v-if,不然会报错,说test组件未注册。v-if是惰性的,只有当第一次值为true时才会开始初始化。
初始化减少DOM的渲染

仍然时上面所说的情况,页面中功能点很多,但是又很多弹窗什么的,其实对于这些弹窗最开始的时候考虑所有的弹窗只使用一个弹窗,为了节约页面初始化的渲染,但是在实际开发过程中,虽然解决了一部分问题,仿佛在开发过程中并不是那么乐观,在弹窗内部出现了大量的v-ifv-show对于维护来说太难了。

也有想过使用<component/>组件,但是一个<component/>所承受的压力可想而知不是一点半点的。但是这个问题任然是存在的需要得到解决。没有办法的情况下,最后使用了两个flag去控制弹窗的显示与隐藏。

<template>
    <div>
        <el-dialog title="提示"
                   v-if="isRenderDialog"
                   :visible.sync="isShowDialog"></el-dialog>
        <el-button @click="onShowDialog">Render Dialog</el-button>
    </div>
</template>

<script>
export default {
    data:() => ({
        isRenderDialog:false,
        isShowDialog:false
    }),
    methods: {
        onShowDialog(){
            !this.isRenderDialog && (this.isRenderDialog = true);
            this.$nextTick(() => {
                this.isShowDialog = true;
            })
        }
    }
}
</script>

上述代码中使用两个flag值控制Dialog一个是控制Dialog的渲染,一个控制Dialog的显示,当用户首次进入页面的时候则dialog元素不会被渲染,当用户点击按钮,对应的Dialog才会被渲染出来,当DialogDOM渲染完成使用在使用显示Dialog

注:在$nextTick中显示dialog是为了保证dialog的动画效果,如果不使用$nextTickdialog就会很生硬的出现。

组件内部请求数据

大家在做业务的时候,可能会有这种情况,当点击按钮之后,需要获取到该条数据的详情渲染到弹窗或者侧滑中,这种情况一定不在少数啦。笔者在开始做这个的时候就是,在点击的时候直接去获取点击的元素的详情数据,当数据返回之后把数据放到data中缓存,之后再传到组件中。

这样做不是不可行的,也是可以的,这样就会面临一个问题,第一点就是当弹窗中的渲染的元素过多的情况下,侧滑或者弹窗的动画效果会很卡,有的时候甚至是不动,瞬间就消失了。

最后经过反复的实验,把数据放到弹窗内部组件中去请求,保证弹窗或者侧滑出现的时候内置元素较少,当数据没有请求回来之前需要把弹框组件内的所有元素隐藏,使用loading代替,当弹窗或者侧滑关闭的使用需要把显示的组件销毁掉,保证里面的数据所占用的内存被释放,这样对于整体优化还是有一些帮助的。

tamplate少计算

由于业务情况的复杂程度,难免会某一个地方添加各种条件的渲染,例如:v-if="isHide && selectList.length && (isA || isB)",这里也只是举一个简单的栗子可能在实际的开发过程中的情况远比这个要复杂的多,这种表达式看上去虽然说是可以维护的,但是长此以往下去就会暴露问题,这样做是很不利于维护的。

对于这种情况可以适当的使用methodscomputed封装成方法,其实这样做的好处是方柏霓我们判断相同的表达式,如果其他的元素也有类似的需求可以直接使用这个方法。

v-for && v-bind:key

在使用v-for循环过程中,使用:key="item.id"这样的代码对于代码的性能是很不友好的,因为当data数据更新的时候,新的状态值会和旧的状态值做对比,Vue在多diff算法的时候能够更快的定位到虚拟DOM的元素上。

其实说到这里就需要说明以下keyvue中到底起到一个什么样的作用,key属性其实是vue的一个优化,上文也说了就是为了更精准高效的定位到虚拟DOM,相当于使用key给数组中某个预算绑定到了一起,如果那个key对应的数据发生了变化,直接更新对应的DOM就可以了。

对于简短的for来说可以直接使用index作为key但是,如果大型列表的话最好还是不要使用index作为key了。举个栗子,例如数组删除了一个元素,那么这个元素后方元素的下标全都前移了一位,之前key对应的数据和dom就会乱了,除非重新匹配key,那就容易产生错误。如果重新匹配key,等于全部重新渲染一遍,违背了使用key来优化更新dom的初衷。但是如果对于Vue玩的很透的同学来说可以可以忽略这一条。

Object.freeze

如果对Vue有一定了解的小伙伴都知道Vue是通过Object.defineProperty对数据进行挟持,来最终实现视图响应数据的变化,但是在实际的开发过程中,页面中有一部分可能不需要进行双向绑定,只是做单纯的渲染,数据一旦绑定之后不需要再做出任何改变的时候可以使用Object.freeze对数据做解绑。

先介绍以下Object.freeze内置函数,用于对接对象,冻结后的对象不会在被修改,不能对这个对象进行添加新属性, 不能删除已有属性,不能修改该对象已有属性的可枚举性,可配置性,可写性.此外冻结一个对象后该对象的原型也不能进行修改。

当数据量大的时候,这能够很明显的减少组件初始化的时间,这里有一个需要注意的点就是一旦被冻结的对象就再也不能被修改了。但是这里有一个问题需要注意的是,嗒嗒嗒,敲黑板!敲黑板!敲黑板!

虽然Object.freeze在一定程度上能够帮助我们提升一部分的数据性能,但是在使用的时候仍然需要谨慎使用。避免造成数据无法响应的问题。如果使用Object.freeze这个属性再次给其对象属性赋值时,则会抛出错误不能分配给对象的只读属性*

用这种方法去提升性能如果数据量小的情况是无法感觉出来的。只有数据量大的时候,才会感觉到数据的明显变化。

渲染前处理

在渲染数据的时候,后端所返回的数据和UI设计图中所需要的数据格式不一致,比如:列表中需要展示一个时间,但是后端返回的是一个时间戳,那么前端就需要对这部分数据进行处理。一般来说处理这种情况有一些办法,使用函数,使用filter,还有就是在渲染之前把数据处理好。

笔者这里比较建议在渲染之前把所有的数据处理好,为什么?数据渲染之后完成之后才会去执行里面的函数或者是过滤器,这样会给页面渲染造成很明显的额外的负担。如果对Vue3.0了解的同学可以知道,在Vue3.0中已经把filter这个功能已经去掉了,推荐使用computed来实现相同相同的效果。

猜测内容:可能尤大大也发现了filter给页面渲染带来的额外的负担,并没有对页面的性能提升起到很大的作用。

functional

不是很多函数组件都需要方法,Vue中为了表示一个模板应该被编译成一个功能组件,在模板中 添加了functional属性。如果项目中的所使用的组件不是有状态的组件,那么就可以使用functional属性把这个组件抓换成功能组件。

功能组件(不要与Vue的render函数混淆)是一个不包含状态和实例的组件。功能组件是一个没有状态或实例的组件。由于功能组件没有状态,因为不需要为Vue的数据响应之类的东西做初始化动作。功能组件仍然会像出入的props一样对数据更新做出响应,但是功能组件的自身,由于它不维护自己的状态,同时也因此无法知道自己的数据是否已经发生了改变。在大型项目中使用功能组件以后,在对于DOM渲染有重大的改进。

由于功能组件没有状态,因此不需要为Vue的反应系统之类的东西进行额外的初始化。功能组件仍然会像传入的新道具那样对更改做出反应,但是在组件本身内,由于它不维护自己的状态,因此无法知道何时数据已更改。

在许多情况下,功能组件可能不合适。毕竟,使用JavaScript框架的目的是构建更具反应性的应用程序。在Vue中,如果没有适当的反应系统,则无法执行此操作。

假设我们的组件接受一个prop.user,该对象是带有firstName和的对象lastName,并且我们想要呈现一个显示用户全名的模板。在功能<template>组件中,我们可以通过在组件定义上提供一个方法,然后使用$optionsVue提供的属性来访问我们的特殊方法来做到这一点:

<template functional>
    <div>{{ $options.userFullName(props.user) }}</div>
</template>
<script>
export default {
    props: {
        user: Object
    },
    userFullName(user) {
        return `${user.firstName} ${user.lastName}`
    }
}
</script>
子组件中处理业务

页面中也会有很多的列表,列表中也会有各种各样的复杂的情况,这个时候可以把一些比较繁重的业务处理存放到其子组件中。

代码对比:

<template>
  <div :style="{ opacity: number / 300 }">
    <div>{{ heavy() }}</div>
  </div>
</template>

<script>
export default {
  props: ['number'],
  methods: {
    heavy () {
      const n = 100000
      let result = 0
      for (let i = 0; i < n; i++) {
        result += Math.sqrt(Math.cos(Math.sin(42)))
      }
      return result
    },
  },
}
</script>

优化后:

<template>
  <div :style="{ opacity: number / 300 }">
    <ChildComp/>
  </div>
</template>

<script>
export default {
  props: ['number'],
  components: {
    ChildComp: {
      methods: {
        heavy () { /* 长任务在子组件里。 */ }
      },
      render (h) {
        return h('div', this.heavy())
      }
    }
  }
}
</script>

当组件随着props:number的变化,组件patch重新渲染的时候,heavy长任务也会重新执行。但是如果能将没有与父组件相互依赖的元素,拆成一个组件,在父组件需要重新渲染的时候,因为与父组件没有依赖子组件并不会跟着重新渲染,响应的性能也能得到提升。

局部作用域

开发过程中会经常使用到一些计算属性或者Util函数,如果我们在循环过程中,不断的使用this.***去调用一个计算属性的时候,每次调用这个值计算属性都会计算一次,然而这个值确是一个固定不变的值,就造成了很大的性能的浪费。

如果当我们使用这些属性的时候,最好的方式是把对应的值取出来,然后再去使用。

<template>
  <div :style="{ opacity: start / 300 }">{{ result }}</div>
</template>
<script>
export default {
  props: ['start'],
  computed: {
    base () {
      return 42
    },
    result ({ base, start }) {
      let result = start
      for (let i = 0; i < 1000; i++) {
        result += Math.sqrt(Math.cos(Math.sin(base))) + base * base + base + base * 2 + base * 3
      }
      return result
    },
  },
}
</script>
总结

以上是我通过调查资料以及个人项目中的一些小经验得出的对于Vue性能优化的一些方案,可能文章中一些见解存在一些问题,欢迎大家在评论区指出,大家一起学习,一起进步。


Aaron
4k 声望6.1k 粉丝

Easy life, happy elimination of bugs.