Skip to content

编写高性能的 Vue 代码

Oct 07, 2023
巷寓
9 min read

将组件渲染所需的数据分为

  • 状态:组件视图变化的 源头,并且它的值会在组件的生命周期中发生 改变(动态)
  • 变量:要么是从状态派生而来的值,要么它的值不会发生改变(静态)

从生命周期的角度进行优化:

首先最小化组件状态,其次,对于状态变化相对独立的模板区块,拆分成组件。

初始化

Vue 在初始化的性能瓶颈主要是:1.创建组件实例;2.响应式数据处理

因此优化围绕上述两点,具体有以下方式:

减少不必要的组件实例

  • 将大多数没有状态的组件改为 函数式组件,提供更轻量的运行时(不生成 vue 实例,无状态,无生命周期)
  • 对于稍微复杂一点的组件,如果使用模板的方式写函数式组件,是比较繁琐的,代码可读性也较差。这里需要 摒弃 template 的写法,使用 render 函数 + jsx

精简 props

Vue 内部会调用 validateProp 函数进行类型校验,在 props 数量较多时,校验的过程耗时较明显。

组件状态治理

  • 区分渲染所需要的状态和变量,变量不进入响应式系统,最小化状态数量
  • 状态动静分离,比如商品信息,大部分数据是静态,只有少部分数据像价格是动态的,这时可以,尽量减少状态数量(因为 vue 会为每个 key 设置响应式)
  • 最小化状态的作用范围:这里用变量作用域的概念做类比,《代码大全》里建议,我们应该让一个变量的的生存周期最小,也就是让它的作用域最小,哪里要用就在哪里声明。vue 组件中的状态也是一样,我们要最小化它们的作用域,最小化它们的生命周期,尽量避免让非常多的 watcher 都依赖同一份状态(dep)。少用 vuex,状态尽量在组件内部;避免无效的状态提升,只让粒度最小的那个组件持有状态
  • 避免滥用 vuex,只将 真正 需要存放在全局的数据放在 vuex 中,因为 vuex 中的数据生命周期较长(app 级),类似全局变量,会导致内存占用持续过高的问题。并且,vuex 中的数据天然响应式,难以做数据静态的优化,如果在大量组件内基于 vuex 数据定义了 computed,则会导致 vuex 数据的 dep 中存储大量的 watcher

变量处理

组件状态一般放在 data 中,变量则

  • 动态的挂载在 this 上(变量没有收敛在一个地方,可维护性差)
  • 依然放在 data 中,但是用 Object.freeze() 进行冻结
    • 对于放在 store 中的全局变量,只能冻结;
    • 冻结后无法增删改对象属性,不太方便

运行时

运行时优化的核心是减少组件渲染次数 & 加快渲染的速度。

减少组件渲染次数

  • 如果确定组件只渲染一次,可以使用 v-once
  • 子组件的 v-if 逻辑最好在父组件里判断,不要在子组件的根节点判断,因为这样会增加子组件的初始化和渲染开销
  • 不滥用 nextTick(特别是在回调中又更改了组件状态的情况),使用的话注意执行时机,尽量在组件渲染末尾执行
  • 排查 immediate watch,以及 created 和 mounted 钩子中的代码是否有 nextTick 调用,如果在 nextTick 中又改变了组件状态,会导致组件在初始化时重复渲染。建议梳理此处逻辑,尽量将状态变更操作前置
  • 渲染数据尽量一次性获取并处理好,避免分批获取数据导致组件多次渲染。对于 加载更多数据的场景,每次加载后不要更改原来数据的引用,也就是 不要做全量替换,而是增量添加,或者直接改变对象中属性的值

加快渲染的速度

  • 分块渲染(延迟渲染):某些场景下,由于组件本身的复杂度高,导致难以直接优化渲染速度。这时候,我们可以采用分块渲染或延迟渲染的方式,一次只渲染部分组件,避免对主线程的长时间占用。比如列表场景下每页 20 个一次性渲染,改为每个 requestAnimationFrame 回调渲染 5 个商品,参考如下代码:

    js
    const getGoodsChunk = (goods, chunkSize) => {
      if (goods.length ===0 ) return 
      requestAnimationFrame(() => {
        const gChunks = goods.slice(0, chunkSize)
    		// res = {goods: []}
        Vue.set(res, 'goods', loadNext ? (res.goods || []).concat(gChunks) : gChunks)
        getGoodsChunk(goods.slice(chunkSize), chunkSize)
        requestAnimationFrame(() => {
          // 分块后执行懒加载
        })
      })
    }
  • 将无参数的、返回一个值的纯函数 method,转为 computed(或变量)

  • 复杂计算做任务拆分,采用时间分片,分步计算

  • 不涉及到 DOM 的任务,可以交给 web worker,减少主线程占用

销毁阶段

提升销毁性能的核心在于减少 watcher 的数量,因为组件销毁时会触发所有相关 watcher 的 teardown 操作。

减少 watcher 方式

  • 减少组件实例: 每个组件实例的 render function 都是一个 watcher, 可以使用函数式组件

  • 减少 computed: 每个 computed 都是一个 watcher

    • 将 computed 从改写为变量,有两种方式
      • 在 render 函数中作为局部变量
      • 需要写 jsx
      • 局部变量,render 函数调用后会 gc
    • 在 created 和 beforeUpdate 钩子中执行相同的数据赋值逻辑
      • 无需写 jsx
      • 变量挂载 this 上,不会 gc
  • 减少用户手动定义的 watcher: 将 watcher 逻辑转移到事件回调中

总结

  • 根据不同场景选择优化手段,如 Vue 的更新粒度是组件级,理论上我们将组件拆分得越细越好。
  • 重展示、轻交互、组件数量多的场景,如商品列表展示,组件拆分过细会导致虽然运行时更新友好,但初始化和销毁性能开销增大
  • 设置 Vue.config.performance = true 后,可以在 performance 面板的 Timings 中查阅各个组件的 init,render,patch 情况,如果有组件不符合预期的重复 render,patch 过程,结合主线程火焰图 + 打断点调试,找到问题代码。

Drama is a song written by IU.