Vue 3 性能优化:组件渲染与更新的底层逻辑与优化策略

Vue 3 性能优化:组件渲染与更新的底层逻辑与优化策略

Vue 3 响应式系统比 Vue 2 强很多,但写不对还是会踩坑。数据变了视图没更新、列表渲染卡顿、组件莫名频繁重新渲染——这些问题大多是因为对 Vue 的渲染机制理解不够深入。

我之前遇到一个典型案例:表格组件里有个下拉框,切换选项时整个表格都重新渲染了 200 多个单元格,LCP 从 800ms 飙升到 3s。排查后发现是响应式数据用错了。

说下 Vue 3 渲染和更新的底层逻辑,以及常见的性能问题怎么排查和解决。

Vue 3 渲染机制概述

组件实例和响应式系统

每个 Vue 3 组件在 setup 阶段会创建一个组件实例,实例里包含:

  • 组件的响应式状态(ref/reactive 定义的数据)
  • 渲染函数(返回虚拟 DOM)
  • 生命周期钩子的注册表
  • 依赖追踪(谁用了哪些响应式数据)

当响应式数据变化时,Vue 会触发依赖收集 → 触发更新的流程:

  1. 渲染函数执行,访问响应式数据(比如 state.count
  2. Vue 记录下"当前组件依赖了 count"
  3. 后续 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>

UserListProductList 组件被缓存后,切换走再切换回来不需要重新创建实例和请求数据。

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.statusitem.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 的响应式系统已经很高效,但写代码时还是要遵循几个原则:

  1. 基本类型用 ref,对象用 reactive,精确控制响应式粒度
  2. v-for 用稳定 id 做 key
  3. 模板里用 computed 替代方法调用
  4. 大列表用虚拟滚动
  5. watch 监听具体的字段而不是整个对象
  6. 频繁切换的组件用 keep-alive 缓存
  7. 用 v-memo 跳过不必要的子组件更新

性能问题通常是渐进的:一次不恰当的 watch、一两个没用 key 的 v-for,当时可能感觉不到,等数据量上来就卡了。养成好的编码习惯,比后期优化省力得多。

最后更新 4/20/2026, 12:15:46 AM