大数据量场景下的Vue性能优化

5,407 阅读4分钟

性能优化最常见的落脚点是在网络和dom上,但是在大数据量的场景下,由于Vue本身的特性,可能会造成js运行层面的性能问题,这篇文章讨论的就是针对这一部分的性能优化方案。

模拟一个大数据量的场景

// App.vue
<template>
    <div>
        <p>It's {{ firstUser.name }}'s show time</p>
        <div>total: {{ total }}</div>
    </div>
</template>

<script>
const user = []
let i = 0
while (i++ < 50000) {
    user.push({
        id: i,
        age: 18,
        name: `kunkun_${i}`,
        alais: 'Irving',
        gender: 'female',
        education: 'senior high school',
        height: 'xxx',
        weight: 'xxx',
        hobby: 'xxx',
        tag: 'xxx',
        skill: {
            sing: 0,
            dance: 0,
            rap: 0,
            basketball: 100,
        },
    })
}

export default {
  data: {
    userList: user,
  },

  computed: {
    firstUser() {
      const userList = this.userList
      return userList.length ? userList[0] : {}
    },

    total() {
      return this.userList.length
    },
  },
}

如以上代码所示,模拟了5万个用户,每一个用户拥有id, name, age等等字段。 jsfiddle

打开chrome devtool的Performance工具,可以看到,渲染这个组件的过程中,Observer这个阶段耗时2.19s(测试机器配置为桌面端i7,16g内存)。

未优化.png

接下来,我会一步一步的把这一段耗时减少到10ms。

分析原因

众所周知,Vue在渲染组件的时候,会对data对象进行改造,遍历data的key,调用defineProperty方法定义它的setter和getter。如果某个字段是Object,或者Array,还会递归的对这个字段进行上诉操作。

通常情况下,这个操作耗时是很短的,但是当数据量非常大的时候,对每一个数据项的每一个字段都进行defineProperty的操作就是一个昂贵的操作,所以性能优化的出发点就是减少defineProperty的次数。

Step 1, 减少无用字段

在这个模拟的例子当中,其实我只需要2个字段,一个是name,一个是id(id甚至也可以不要),所以我把多余的字段都去掉,一共减少了8个String类型的字段,和一个Object类型的字段,可以减少 (8 + 4) * n次defineProperty操作和n次递归调用。看看结果如何。

去除无用字段.png

Observer这个操作从2.2s减少到了515ms,提升还是比较大的。

Step2,数据扁平化

在当前版本(2.x)的Vue当中,对于数据变动的检测有许多限制,比如不能检测对象属性的添加和删除;不能检测到通过数据索引直接设置数据项等等。

所以,当一个数组的数据项都是基本数据类型的时候,Vue不会进行任何操作

首先,把user数据拍扁

const user = []
let i = 0
while (i++ < 50000) {
    user.push(`kun_${i}`, i) // 通过index为基数还是偶数分辨是name还是id
}

然后,相应的改变computed的计算方法,不影响渲染逻辑和业务逻辑

...
computed: {
    firstUser() {
        const userList = this.userList
        return userList.length ? { name: userList[0], id: userList[1] } : {}
    },

    total() {
        return this.userList.length / 2
    },
}
...

jsfiddle - 数据扁平化

看看结果如何

扁平数据.png
从上图可以看出,结果非常的明显,从515ms直接减少到了7ms,几乎完全避免了性能损耗。

Step3,利用computed

到此为止,性能上的问题已经解决了,但是扁平的数据会影响业务代码的开发效率和可读性,同时数据和它的index产生了深耦合,如果我们需要添加一个字段使用或者改变下顺序,很容易出问题。 不过,我们可以利用computed计算属性把已经被拍扁的数据重新组装起来。由于Vue的响应式数据改造只针对data选项和props选项,不包括computed,所以只会产生很少的函数运行耗时。

export default {
    data() {
        return {
            // 扁平的数据存起来
            originSserList: user,
        }
    },

    computed: {
        firstUser() {
            const userList = this.userList
            return userList.length ? userList[0] : {}
        },

        total() {
            return this.userList.length
        },

        // 重新'组装'便于使用的计算属性,不影响原本的渲染和业务逻辑
        userList() {
            const result = []
            const user = this.originSserList
            for(let i = 0; i < user.length; i += 2) {
                const name = user[i]
                const id = user[i + 1]
                result.push({ name, id })
            }
            return result
        },
    },
}

看看这种情况下的Performance。

computed+扁平.png
仅仅只是多出了10ms的函数运行时间。

到这里,在无需改动任何的渲染逻辑和业务逻辑的情况下,将js的运行时间从2.2s减少到了10ms左右,提升了200倍。并且这些数据是在桌面端i7处理器下得到的,大大超越了绝大部分的用户的机器性能,更不用说移动端了,所以在实际的大数据量场景下,能取得更加明显的用户可感知的性能提升。

jsfiddle - 数据扁平化 + computed

Step4,数据静态化

没想到吧,还有Step4?已经没有优化空间了呀。

在这个模拟的场景里面,确实没有优化的空间了,不过,并不是所有的数据都可以很好的进行扁平化处理,这可能涉及到方方面面的原因与权衡。那么这种情况下,如何进行优化呢?

通常在Vue组件当中,都是把数据放在data选项当中,Vue会对data选项中的数据进行响应式改造,我称之"动态数据"或者"响应式"数据,但是并不是所有的数据都是会发生变化的,很多时候,特别是大数据量场景下的数据是不会或者很少发生变化的,这种情况下,就没有必要把它放到data选项中去,而是在beforeCreated当中进行数据初始化,也不会影响数据的使用。

beforeCreated() {
    this.userList = xxx // 记得把data当中的userList删掉
}

这种处理方式,我称之为数据静态化,这种数据,我称之为"静态数据"

但是,有一点需要特别的注意,静态数据并不在Vue的响应式系统当中,也就是说当你进行this.userList = newUserList时,视图不会重新渲染,对应的computed计算属性也不会重新计算。没有了Vue提供的响应式系统,如果数据变动的时候,我们需要手动的去计算对应的数据,可能还需要配合$forceUpdate这个api去重新渲染视图。此时,需要在性能和代码可读性与开发效率上进行取舍与权衡。

总结

由于Vue的响应式系统,大数据量场景下可能会造成js运行层面的性能问题,可以通过3个方法去解决

  • 减少无用字段
  • 数据扁平化
  • 数据静态化

这3个方法相互并不冲突,可以根据实际情况选择其中的1种或多种方法进行组合。