打造高性能的 Vue3 组件

685 阅读7分钟

Vue3 组件性能优化的关键点

  • 使用 keep-alive 缓存组件
  • 使用 shallowRef 优化大数据场景
  • 保证计算属性稳定性
  • 避免不必要的组件抽象
  • props 稳定性
  • 路由懒加载
  • 减少 DOM,例如虚拟滚动
  • 列表渲染绑定 key

实际场景使用

keep-alive 缓存组件

在以下场景时使用 <keep-alive> 可以带来以下性能提升:

  1. 减少组件重建次数:每次组件切换时,Vue 不再销毁组件实例,而是从缓存中恢复状态,避免了重复的创建和销毁过程。
  2. 保持状态:组件内的状态(如表单输入内容)不会因为组件卸载而消失,从而提升了用户体验。

keepAlive.gif

使用如下方代码, 如果将组件本身传递给 is 而不是其名称,则不需要注册,例如在 <script setup> 中。

<template>
    <div>
      <button @click="currentTab = TabA">Tab A</button>
      <button @click="currentTab = TabB">Tab B</button>
      <button @click="currentTab = TabC">Tab C</button>
      <keep-alive>
        <component :is="currentTab"></component>
      </keep-alive>
    </div>
  </template>
  
  <script setup>
  import { shallowRef } from 'vue';
  import TabA from './TabA.vue';
  import TabB from './TabB.vue';
  import TabC from './TabC.vue';
  
  const currentTab = shallowRef(TabA);
  </script>
<template>
    <div>
      <h2>Tab A</h2>
      <p>This is the content of Tab A.</p>
      <input type="text">
    </div>
  </template>
  
  <script setup>
  import { onActivated, onDeactivated } from 'vue';
  
  onActivated(() => {
    console.log('Tab A is activated');
  });
  
  onDeactivated(() => {
    console.log('Tab A is deactivated');
  });
  </script>

shallowRef 优化大数据场景

image.png

  • sum1 (使用 ref :每次访问 arr1.value[i] 时,都会通过代理对象进行,并且会触发依赖追踪机制。这些额外的开销导致遍历速度变慢。
  • sum2 (普通数组) :这是一个普通的数组,没有经过 Vue 的响应式处理,因此遍历速度最快。
  • sum3 (使用 shallowRef shallowRef 只将数组本身变成响应式,数组中的元素不是响应式的,因此访问 arr3.value[i] 时没有额外的开销,遍历速度比 ref 快,但比普通数组稍慢。
<script setup lang="ts">
import { ref, reactive, computed, watch, shallowRef } from 'vue';
const arr1 = ref(new Array(1000).fill(1));
const arr2 = new Array(1000).fill(1);
const arr3 = shallowRef(new Array(1000).fill(1));
let sum1 = 0;
let sum2 = 0;
let sum3 = 0;

console.time('ref arr sum');
for(let i =0; i<arr1.value.length ; i++) {
    sum1+=arr1.value[i];
}
console.timeEnd('ref arr sum');

console.time('arr sum');
for(let i =0; i<arr2.length ; i++) {
    sum2+=arr2[i];
}
console.timeEnd('arr sum');

console.time('shallowRef arr sum');
for(let i =0; i<arr3.value.length ; i++) {
    sum3+=arr3.value[i];
}
console.timeEnd('shallowRef arr sum');
</script>

Vue 响应式系统,依赖追踪过程

  • 触发 get 操作:访问响应式对象的属性时,Proxy 拦截 get 操作。
  • 调用 track 函数:在 get 操作的拦截器中调用 track 函数。
  • 检查是否允许收集依赖track 函数检查是否允许收集依赖。
  • 检查当前是否有正在执行的副作用函数track 函数检查当前是否有正在执行的副作用函数。
  • 收集依赖:将当前的副作用函数添加到目标对象的依赖列表中。
  • 存储依赖关系:依赖关系存储在 targetMap 中,建立从数据属性到副作用函数的映射关系。

shallowRef 使用场景

  • fetch 列表数据,列表数据每次都是通过再次请求,然后通过 shallowRef.value 更新。
  • 定义响应式对象嵌套属性不需要更新视图的场景,或者通过 shallowRef.value 更新。
  • 初始化变量不需要是完全响应式的,例如前面的初始化组件变量。
import TabA from './TabA.vue'; 
import TabB from './TabB.vue'; 
import TabC from './TabC.vue'; 
// 这里的 TabA 组件对象内部属性不需要是响应式了,
// 只需要切换 tab 时替换整个组件对象
const currentTab = shallowRef(TabA); 
  • 第三方库需要是响应式时使用
let mychart = shallowRef(null)
onMounted(()=>{
    createNewChart()
})

function createNewChart(){
  // 启用新对象
  mychart.value = echarts.init(document.getElement('chart'))
}

保证计算属性稳定性

我们通常将计算逻辑封装在计算属性。

  1. 这样不仅可以保持模版 template 的简洁,
  2. 计算属性同时还提供缓存,当我们的依赖的响应式状态未发生变更时,无需再次计算。

但是如果你返回的计算属性是对象时需要注意,可能你的写法导致并没有缓存计算结果。

  const computedObj = computed(() => {
    return {
      isEven: count.value % 2 === 0
    };
  });

上面的代码中,由于每次都会创建一个新对象,因此从技术上讲,新旧值始终不同。所以每当 count.val 发生变更的时候,无论 isEven 是否相同都会触发更新。

computed未优化.gif 未优化 computed,即使视图更新前后内容相同,也会触发子组件更新。

优化

来看下 computed 语法,它的 getter 回调回接受一个 oldValue 作为参数,第一次执行时 oldValue 为 undefined。

image.png

  • 定义一个 newValue 对象
  • 判断 newValue 对象是否和 oldValue 的属性是否相同,如果相同则返回 oldValue,无需渲染视图
  • 否则返回 newValue,重新渲染视图
const computedObj = computed((oldValue) => {
  const newValue = {
    isEven: count.value % 2 === 0
  }
  if (oldValue && oldValue.isEven === newValue.isEven) {
    return oldValue
  }
  return newValue
})

computed 优化后.gif

优化后判断对象前后 isEven 是否相同,如果相同则返回 oldValue,则不需要更新组件。

避免不必要的组件抽象

组件实例比普通 DOM 节点要昂贵得多,而且为了逻辑抽象创建太多组件实例将会导致性能损失。

考虑这种优化的最佳场景还是在大型列表中。想象一下一个有 100 项的列表,每项的组件都包含许多子组件。在这里去掉一个不必要的组件抽象,可能会减少数百个组件实例的无谓性能消耗。

来看下实际的例子:

 <div>
     <UserItem v-for="user in users" :key="user.id" :user="user" />
 </div>

在 UserItem 子组件中我们又拆分了三个子组件。这三个子组件内容很简单,渲染头像、名字、行为按钮。

<template>
    <div class="user-item">
      <UserAvatar :user="user" />
      <UserName :user="user" />
      <UserActions :user="user" />
    </div>
  </template>
<template>
    <img :src="user.avatar" :alt="user.name" class="user-avatar" />
  </template>
<template>
    <div class="user-name">{{ user.name }}</div>
  </template>
<template>
    <div class="user-actions">
      <button @click="editUser" class="action-button">Edit</button>
      <button @click="deleteUser" class="action-button">Delete</button>
    </div>
  </template>

性能对比-优化前

我们使用 Performance 来测试下 100 条数据的性能。为了方便查看我们使用 performance.mark 添加标记 和 performance.measure 测量时间间隔。具体代码如下,我们来看下性能面板。

<template>
    <div>
      <UserItem v-for="user in users" :key="user.id" :user="user" />
    </div>
  </template>
  
  <script setup>
  import { onBeforeMount, onMounted } from 'vue'
//   import UserItem from './UserItem.vue';
  import UserItem from './UserItem2.vue';
  onBeforeMount(()=> {
    performance.mark('onBeforeMount')
  })

  onMounted(() => {
    performance.mark('onMounted')
    performance.measure('measure --- renderTime', 'onBeforeMount', 'onMounted');
    const entry = performance.getEntriesByName('measure --- renderTime')[0];
    console.log(`渲染时间: ${entry.duration} ms`);
  })
  const users = Array.from({ length: 100 }, (_, index) => ({
    id: index + 1,
    name: `User${index + 1}`,
    avatar: 'http://gips2.baidu.com/it/u=195724436,3554684702&fm=3028&app=3028&f=JPEG&fmt=auto?w=1280&h=960'
  }));
  </script>

优化前,我们可以看到列表组件的挂载花了 34.18ms。
我们知道,人的眼睛大约每秒可以看到60帧,那么每16.7ms就要看到1帧。如果某个任务的执行时间超过16.7ms,就可能导致在这一时间段内无法完成一帧的渲染,从而使得用户感觉到卡顿。

image.png

性能对比-优化后

直接将简单的三个子组件合并到 UserItem 组件中避免创建太多组件实例将会导致性能损失。

<template>
    <div class="user-item">
      <img :src="user.avatar" alt="User Avatar" />
      <div>{{ user.name }}</div>
      <button @click="editUser">Edit</button>
      <button @click="deleteUser">Delete</button>
    </div>
  </template>

image.png

可以看到单单一个组件拆分到优化就提升了 18ms!

props 稳定性

在 Vue 之中,一个子组件只会在其至少一个 props 改变时才会更新。思考以下示例:

<ListItem
  v-for="item in list"
  :id="item.id"
  :active-id="activeId" />

在 <ListItem> 组件中,它使用了 id 和 activeId 两个 props 来确定它是否是当前活跃的那一项。虽然这是可行的,但问题是每当 activeId 更新时,列表中的每一个 <ListItem> 都会跟着更新!

理想情况下,只有活跃状态发生改变的项才应该更新。我们可以将活跃状态比对的逻辑移入父组件来实现这一点,然后让 <ListItem> 改为接收一个 active prop:

<ListItem
  v-for="item in list"
  :id="item.id"
  :active="item.id === activeId" />

现在,对于大多数的组件来说,activeId 改变时,它们的 active prop 都会保持不变,因此它们无需再更新。总结一下,这个技巧的核心思想就是让传给子组件的 props 尽量保持稳定。

最后

本文探讨了 Vue 3 组件性能优化的几个关键点,包括使用 keep-alive 缓存组件、优化大数据场景下的响应式状态管理、确保计算属性的稳定性、避免不必要的组件抽象、维护 props 的稳定性。

如果你有更多 Vue3 组件性能优化的策略,欢迎在评论区留言讨论👏