Vue3 组件性能优化的关键点
- 使用
keep-alive缓存组件 - 使用 shallowRef 优化大数据场景
- 保证计算属性稳定性
- 避免不必要的组件抽象
- props 稳定性
- 路由懒加载
- 减少 DOM,例如虚拟滚动
- 列表渲染绑定 key
实际场景使用
keep-alive 缓存组件
在以下场景时使用 <keep-alive> 可以带来以下性能提升:
- 减少组件重建次数:每次组件切换时,Vue 不再销毁组件实例,而是从缓存中恢复状态,避免了重复的创建和销毁过程。
- 保持状态:组件内的状态(如表单输入内容)不会因为组件卸载而消失,从而提升了用户体验。
使用如下方代码, 如果将组件本身传递给 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 优化大数据场景
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'))
}
保证计算属性稳定性
我们通常将计算逻辑封装在计算属性。
- 这样不仅可以保持模版 template 的简洁,
- 计算属性同时还提供缓存,当我们的依赖的响应式状态未发生变更时,无需再次计算。
但是如果你返回的计算属性是对象时需要注意,可能你的写法导致并没有缓存计算结果。
const computedObj = computed(() => {
return {
isEven: count.value % 2 === 0
};
});
上面的代码中,由于每次都会创建一个新对象,因此从技术上讲,新旧值始终不同。所以每当 count.val 发生变更的时候,无论 isEven 是否相同都会触发更新。
未优化 computed,即使视图更新前后内容相同,也会触发子组件更新。
优化
来看下 computed 语法,它的 getter 回调回接受一个 oldValue 作为参数,第一次执行时 oldValue 为 undefined。
- 定义一个 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
})
优化后判断对象前后 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,就可能导致在这一时间段内无法完成一帧的渲染,从而使得用户感觉到卡顿。
性能对比-优化后
直接将简单的三个子组件合并到 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>
可以看到单单一个组件拆分到优化就提升了 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 组件性能优化的策略,欢迎在评论区留言讨论👏