FORMA

运行时优化

减少不必要的渲染与响应式开销。构建侧见 build

运行时优化旨在减少不必要的 DOM 操作、响应式开销和计算成本,从而提升应用的交互流畅度和响应速度。以下是 Vue 3 中关键的运行时优化策略。

一、列表渲染优化

1. v-forv-if 共用的冲突与解决方案

在同一个元素上同时使用 v-forv-if 时,Vue 会先执行 v-for 再执行 v-if(因为 v-for 优先级更高),这意味着即使只有少数项需要渲染,也会遍历整个列表,造成性能浪费。Vue 官方不推荐这种写法,并会在开发模式下给出警告。

❌ 错误示例

html
<li v-for="user in users" v-if="user.isActive" :key="user.id">{{ user.name }}</li>

✅ 解决方案:使用计算属性过滤

vue
<script setup>
import { computed } from "vue";
const activeUsers = computed(() => users.value.filter((u) => u.isActive));
</script>
<template>
  <li v-for="user in activeUsers" :key="user.id">
    {{ user.name }}
  </li>
</template>

这样只遍历需要渲染的数据,且计算属性会缓存结果,只在依赖变化时重新计算。

替代方案:在外层包裹 <template> 分别使用 v-ifv-for,但不推荐。

2. key 的作用机制与使用原则

key 是 Vue 虚拟 DOM 算法中用于标识节点复用的重要属性。当列表重新排序、插入或删除时,正确的 key 可以最大化复用已有 DOM 元素,减少不必要的 DOM 操作。

  • 使用唯一 ID(如数据库主键、uuid):<li v-for="item in list" :key="item.id">
  • 避免使用数组索引作为 key:当列表顺序变化或中间插入/删除元素时,索引会导致错误的节点复用,引发状态错乱和性能下降。
vue
<!-- ❌ 坏:用索引作为 key -->
<li v-for="(item, index) in list" :key="index">
<!-- ✅ 好:用唯一标识 -->
<li v-for="item in list" :key="item.id">

若列表静态且不会变化,也可以不用 key(Vue 会用就地复用策略),但在大多数动态场景下,提供稳定的 key 是优化和防止 bug 的必要手段。

3. 计算属性 vs 方法的选择

  • 计算属性(computed:基于响应式依赖缓存,只有当依赖的数据发生变化时才重新求值。适合派生状态、数据转换、过滤列表等场景。
  • 方法(methods:每次重新渲染都会执行,没有缓存。适合需要副作用每次调用都需要最新值的操作(如时间格式化、随机数)。
js
// 计算属性:只会在 users 变化时重新计算
const activeUsers = computed(() => users.value.filter((u) => u.active));

// 方法:每次渲染都会执行
function getActiveUsers() {
  return users.value.filter((u) => u.active);
}

在模板中多次引用 activeUsers,计算属性只会执行一次过滤;而方法调用几次就执行几次。优先使用计算属性

4. 虚拟滚动(万级列表渲染优化)

当需要渲染大量列表项(成千上万)时,直接使用 v-for 会导致 DOM 节点过多、内存占用高、滚动卡顿。虚拟滚动(Virtual Scrolling)的核心思想是:只渲染可视区域内的元素,以及上下少量的缓冲项,随着滚动动态更新渲染内容。

实现原理

  • 监听容器的 scroll 事件,计算当前滚动位置。
  • 根据每个项的高度(固定或预估)确定可视区起始索引和结束索引。
  • 仅渲染该区间的 DOM,并通过 transform: translateYpadding 模拟完整列表的滚动高度。

Vue 生态常用库

  • vue-virtual-scroller(官方推荐)
  • vue3-virtual-scroll-list
  • tanstack-virtual(框架无关)

使用示例(vue-virtual-scroller):

vue
<template>
  <RecycleScroller :items="list" :item-size="50" key-field="id">
    <template #default="{ item }">
      <div class="item">{{ item.name }}</div>
    </template>
  </RecycleScroller>
</template>

对于表格、瀑布流等复杂场景,虚拟滚动能极大提升渲染性能。

二、复杂对象性能处理

当处理大型、深度嵌套的对象时,全量响应式代理(reactive)会带来性能开销:初始化时递归遍历所有属性,每个属性都成为响应式数据。可通过以下 API 降低开销。

1. shallowRef / shallowReactive 用于大对象

  • shallowReactive:只代理对象的第一层属性,深层属性保持原始(非响应式)。
  • shallowRef:只追踪 .value 的引用变化,如果 .value 是对象,对象内部变化不会触发更新。
js
import { shallowReactive } from "vue";
const bigData = shallowReactive({
  meta: { version: 1 }, // meta 内部变化不会触发更新
  items: [
    /* 大量数据 */
  ], // items 内部变化不会触发更新
});
// 只有替换整个 bigData.meta 或 bigData.items 才会触发更新

适用场景:展示静态配置、不需要响应式深层的第三方实例、可整体替换的大数据集。

注意:一旦使用浅响应式,修改深层属性不会导致视图更新,需谨慎或配合 triggerRef 强制更新。

2. markRaw 标记永久不可响应化的数据

markRaw 标记的对象或值将永远不会被代理,即使传入 reactiveref 也会保持原始状态。

js
import { markRaw, reactive } from "vue";
const externalLib = markRaw(new SomeLibrary());
const state = reactive({ lib: externalLib }); // lib 不会被代理

典型场景

  • 第三方类实例(如 ECharts、Map 实例)。
  • 永远不需要响应式的巨大数据。
  • 不可变的外部数据源。

收益:避免不必要的性能开销,同时防止意外触发响应式更新。

三、watch vs watchEffect

API特点适用场景
watch懒执行,指定数据源,可访问旧值,可配置 { deep, immediate }需要监听特定数据、需要旧值、需要控制深度
watchEffect立即执行,自动收集依赖,无法获取旧值副作用逻辑简单,依赖较多时自动追踪

1. 深度监听(deep: true)的性能代价

当对一个大型深层对象使用 watchdeep: true 时,Vue 需要递归遍历对象的所有属性以检测变化。如果对象层级深、体积大,这会造成显著的 CPU 开销。

js
// ❌ 避免在大型对象上使用 deep
watch(
  () => state.bigObject,
  (newVal) => {
    // 任何深层属性变化都会触发
  },
  { deep: true },
);

更好的做法

  • 监听特定路径:使用 watch(() => state.bigObject.someField, ...)
  • 使用计算属性派生需要的值:如果只需要某个计算结果,使用 computed 可自动缓存。
  • 手动浅比较:对于嵌套对象,可以在需要的地方手动 triggerRef

2. 优先使用计算属性替代深度监听

许多场景下,我们其实并不需要监听整个对象的变化,而是需要基于对象派生某个状态。计算属性天然具备缓存和依赖追踪,比深度监听更高效。

js
// 坏:深度监听
watch(
  () => state.user,
  (user) => {
    fullName.value = `${user.firstName} ${user.lastName}`;
  },
  { deep: true },
);

// 好:计算属性自动追踪具体依赖
const fullName = computed(() => `${state.user.firstName} ${state.user.lastName}`);

如果必须监听深度改变(例如需要保存整个表单到服务器),可以考虑防抖仅在用户提交时序列化,而非每次变化都深度遍历。

四、其他运行时优化提示

  • v-once:标记静态内容,只渲染一次,后续不再更新。适用于纯静态的文本或元素。
  • v-memo(Vue 3.2+):记忆化子树,仅在依赖数组变化时才重新渲染。适合子组件列表。
  • 使用 shallowRef 处理不需要响应式的组件实例:例如 const map = shallowRef(null) 存储地图实例。
  • 异步组件 + Suspense:将非关键组件异步加载,减少初始渲染压力。

总结

优化主题关键手段
列表渲染分离 v-forv-if(用计算属性);使用唯一 key;优先计算属性而非方法;万级列表使用虚拟滚动
大对象处理shallowRefshallowReactivemarkRaw 避免深度响应式开销
侦听器优化避免大对象深度监听;用计算属性取代深度 watch;明确监听目标路径
其他v-oncev-memo、异步组件、shallowRef 外部实例

运行时优化往往是细节的堆叠。可结合 Lighthouse 与 Chrome Performance 面板定位瓶颈。

参考文献

以下链接在编写时均可正常访问:

资料说明
Vue:列表渲染v-for 与 key
Vue:性能官方建议
Vue:v-memo记忆化
web.dev:Web Vitals核心指标(英文)

Series

performance

3 / 3