Vue 3 性能优化:组件渲染与更新的底层逻辑与优化策略
Vue 3 性能优化:组件渲染与更新的底层逻辑与优化策略
Vue 3 响应式系统比 Vue 2 强很多,但写不对还是会踩坑。数据变了视图没更新、列表渲染卡顿、组件莫名频繁重新渲染——这些问题大多是因为对 Vue 的渲染机制理解不够深入。
我之前遇到一个典型案例:表格组件里有个下拉框,切换选项时整个表格都重新渲染了 200 多个单元格,LCP 从 800ms 飙升到 3s。排查后发现是响应式数据用错了。
说下 Vue 3 渲染和更新的底层逻辑,以及常见的性能问题怎么排查和解决。
Vue 3 渲染机制概述
组件实例和响应式系统
每个 Vue 3 组件在 setup 阶段会创建一个组件实例,实例里包含:
- 组件的响应式状态(ref/reactive 定义的数据)
- 渲染函数(返回虚拟 DOM)
- 生命周期钩子的注册表
- 依赖追踪(谁用了哪些响应式数据)
当响应式数据变化时,Vue 会触发依赖收集 → 触发更新的流程:
- 渲染函数执行,访问响应式数据(比如
state.count) - Vue 记录下"当前组件依赖了
count" - 后续
count变化时,Vue 知道要重新渲染"依赖了 count 的组件"
关键概念:响应式追踪是粒度级别的
Vue 3 的响应式追踪是按属性的,不是按组件。这意味着:
const state = reactive({ count: 0, name: '' })
// 组件 A 只用了 count
// 组件 B 只用了 name
// count 变化时,只有组件 A 重新渲染
// name 变化时,只有组件 B 重新渲染
这比 Vue 2 的响应式(整个组件级别)高效很多,但前提是:你要正确地拆分响应式数据。
常见性能问题一:响应式数据用错导致不必要的重新渲染
问题代码
<script setup>
import { reactive } from 'vue'
// 问题:整个表单状态放一个 reactive 对象
const form = reactive({
username: '',
password: '',
rememberMe: false,
// ... 20 个字段
})
// 这个组件只关心 username,但 form 任何字段变化都会触发重新渲染
</script>
<template>
<input v-model="form.username" />
</template>
正确做法
<script setup>
import { ref } from 'vue'
// 精确拆分:只把用到的数据变成响应式
const username = ref('')
const password = ref('')
</script>
<template>
<input v-model="username" />
</template>
什么时候用 reactive 什么时候用 ref
- 基本类型(string/number/boolean)用 ref
- 对象/数组如果整体使用,用 reactive
- 对象/数组如果只读取部分属性,考虑拆分或者用
toRefs
// 场景一:表单对象整体使用
const form = reactive({ username: '', password: '' })
// form 任何字段变化,整个 form 对象都会触发响应
// 场景二:只关心特定字段
const username = ref('')
const password = ref('')
// username 变化只触发依赖它的组件更新
常见性能问题二:v-for 用的 key 不稳定
问题代码
<template>
<div v-for="item in list" :key="item">
{{ item.name }}
</div>
</template>
用对象或数组本身做 key 有问题,因为它们引用不稳定。
正确做法
<template>
<div v-for="item in list" :key="item.id">
{{ item.name }}
</div>
</template>
key 的作用是帮助 Vue 的虚拟 DOM 复用算法( reconciliation )定位每个节点。如果 key 不稳定,Vue 会认为每个节点都是新创建的,导致不必要的 DOM 操作和组件重新挂载。
常见性能问题三:computed 不当使用
问题代码
<script setup>
const { data } = useFetch()
// 问题:在模板里调用方法而不是使用 computed
</script>
<template>
<div>{{ formatDate(data.date) }}</div>
<div>{{ formatMoney(data.amount) }}</div>
<div>{{ formatStatus(data.status) }}</div>
</template>
模板里调用方法,每次渲染都会执行这些方法,不管数据有没有变化。
正确做法
<script setup>
const { data } = useFetch()
// 用 computed 缓存计算结果
const formattedDate = computed(() => formatDate(data.date))
const formattedAmount = computed(() => formatMoney(data.amount))
const formattedStatus = computed(() => formatStatus(data.status))
</script>
<template>
<div>{{ formattedDate }}</div>
<div>{{ formattedAmount }}</div>
<div>{{ formattedStatus }}</div>
</template>
常见性能问题四:大列表没有虚拟滚动
问题
如果表格有 1000+ 行,Vue 会创建 1000 个组件实例,DOM 节点过多导致页面卡顿。
解决:用虚拟滚动
安装 vue-virtual-scroller:
npm install vue-virtual-scroller
<template>
<RecycleScroller
class="list"
:items="largeList"
:item-size="50"
key-field="id"
v-slot="{ item }"
>
<div class="list-item">
{{ item.name }}
</div>
</RecycleScroller>
</template>
<style>
.list {
height: 500px;
overflow-y: auto;
}
</style>
vue-virtual-scroller 只渲染可视区域内的元素(大约 10-20 个),滚动时动态复用 DOM 节点,大大减少 DOM 操作。
如果列表数据量在 100 以内,其实不需要虚拟滚动,普通的 v-for 就够了。
常见性能问题五:watch 和 watchEffect 使用不当
问题代码
<script setup>
import { watch } from 'vue'
const form = reactive({ keyword: '', status: '', dateRange: [] })
// 问题:监听整个 form 对象,且开启 deep
watch(form, () => {
fetchData() // form 任何嵌套属性变化都会触发
}, { deep: true })
</script>
正确做法
<script setup>
import { watch, watchEffect } from 'vue'
const form = reactive({ keyword: '', status: '', dateRange: [] })
// 方案一:只监听特定的字段
watch(
() => form.keyword,
(newVal) => {
fetchData()
}
)
// 方案二:用 watchEffect,但明确依赖
watchEffect(() => {
// 只有 form.keyword 和 form.status 变化时才会执行
const _ = form.keyword + form.status
fetchData()
})
组件级别的性能优化
KeepAlive 缓存不活跃的组件
对于切换频繁但数据不常变化的组件,用 keep-alive 缓存:
<template>
<router-view v-slot="{ Component }">
<keep-alive :include="['UserList', 'ProductList']">
<component :is="Component" />
</keep-alive>
</router-view>
</template>
UserList 和 ProductList 组件被缓存后,切换走再切换回来不需要重新创建实例和请求数据。
v-memo 条件性跳过更新
Vue 3.2+ 引入的 v-memo 可以跳过满足条件的更新:
<template>
<div v-for="item in list" :key="item.id" v-memo="[item.status, item.category]">
<ComplexComponent :item="item" />
</div>
</template>
v-memo 的第二个参数是依赖数组。当 item.status 和 item.category 没变化时,即使 list 重新渲染,这个 item 也不会重新渲染。
shallowRef 和 shallowReactive
对于大型数据结构,只想追踪顶层变化时用 shallowRef / shallowReactive:
// 只追踪引用变化,不追踪深层属性
const state = shallowRef({
items: [] as Item[],
metadata: { total: 0 },
})
// 替换整个 items 数组会触发更新
state.value.items = newItems // 触发更新
// 直接 push 不会触发更新
state.value.items.push(newItem) // 不触发更新!
性能排查工具
Vue DevTools
打开 DevTools 的 Component inspector 和 Timeline,可以看每个组件的渲染次数和耗时。
Chrome DevTools Performance 面板
录一段用户操作,分析:
- 哪些函数执行时间过长
- 哪些组件重新渲染了
- 布局抖动(Layout Thrashing)在哪里
vue/extension 的 Performance 标记
Vue 3 支持在 window.__VUE_PROD_DEVTOOLS__ 开启时,在 Performance 面板里标记 Vue 相关的操作。
总结
Vue 3 的响应式系统已经很高效,但写代码时还是要遵循几个原则:
- 基本类型用 ref,对象用 reactive,精确控制响应式粒度
- v-for 用稳定 id 做 key
- 模板里用 computed 替代方法调用
- 大列表用虚拟滚动
- watch 监听具体的字段而不是整个对象
- 频繁切换的组件用 keep-alive 缓存
- 用 v-memo 跳过不必要的子组件更新
性能问题通常是渐进的:一次不恰当的 watch、一两个没用 key 的 v-for,当时可能感觉不到,等数据量上来就卡了。养成好的编码习惯,比后期优化省力得多。
