减少 Vue 组件的重渲染是提升应用性能的关键。Vue 的响应式系统虽然强大,但如果使用不当,会导致不必要的组件更新。
以下是基于 Vue 3(部分适用于 Vue 2)的核心优化策略,涵盖了你提到的
v-once、computed、shallowRef 以及其他关键技巧:1. 静态内容优化:v-once
如果你确定某部分内容在初始化后永远不会改变,可以使用
v-once 指令。- 原理:告诉 Vue 跳过该节点及其子节点的后续更新检查。
- 适用场景:静态文本、图标、不随状态变化的布局结构。
- 代码示例:
html预览
1<!-- 这个 span 只会渲染一次,即使 message 改变,这里也不会更新 --> 2<span v-once>这是一个静态标题:{{ title }}</span> 3 4<!-- 整个组件只渲染一次 --> 5<my-static-component v-once /> - 注意:不要滥用,否则会导致数据无法更新。
2. 缓存计算属性:computed
避免在模板中直接进行复杂的逻辑运算或对象创建。
- 原理:
computed属性基于其响应式依赖进行缓存。只有当依赖项发生变化时,它才会重新求值。如果依赖未变,多次访问会直接返回之前的缓存结果。 - 对比:
- ❌ Methods: 每次组件重渲染(即使无关数据变化),方法都会重新执行。
- ✅ Computed: 仅当依赖变化时执行。
- 代码示例:
javascript编辑
1// ❌ 糟糕的做法:每次 render 都会执行过滤和排序 2// <div v-for="item in filteredList()">{{ item.name }}</div> 3// methods: { filteredList() { return this.list.filter(...) } } 4 5// ✅ 推荐做法:只有 list 变化时才重新计算 6const filteredList = computed(() => { 7 return props.list.filter(item => item.active).sort((a, b) => a.id - b.id); 8});
3. 深层响应式优化:shallowRef 和 markRaw
这是 Vue 3 中针对大型对象或不需要深层响应式数据的强力优化手段。
A. shallowRef (浅层引用)
- 原理:只追踪
.value本身的赋值变化,不深入追踪对象内部属性的变化。 - 适用场景:
- 巨大的嵌套对象(如地图数据、大型图表配置),且你只需要在替换整个对象时触发更新。
- 由外部库管理的状态对象(如 Redux store, Pinia 中的某些部分),Vue 不需要代理它们。
- 代码示例:
javascript编辑
1import { shallowRef, triggerRef } from 'vue'; 2 3// 创建一个巨大的对象 4const hugeData = shallowRef({ a: 1, b: { c: 2, d: 3 ... } }); 5 6// 修改内部属性不会触发视图更新 (节省了大量 Proxy 开销) 7hugeData.value.b.c = 999; // ❌ 视图不更新 8 9// 必须手动触发更新,或者替换整个对象 10hugeData.value = { ...hugeData.value, b: { ...hugeData.value.b, c: 999 } }; // ✅ 触发更新 11// 或者 12triggerRef(hugeData); // ✅ 强制触发更新
B. markRaw (标记原始对象)
- 原理:明确告诉 Vue 不要将某个对象转换为 Proxy(响应式对象)。
- 适用场景:
- 第三方类实例(如
new Map(),new Date(), 或复杂的 UI 库实例)。 - 包含循环引用的对象。
- 第三方类实例(如
- 代码示例:
javascript编辑
1import { reactive, markRaw } from 'vue'; 2 3const state = reactive({ 4 user: { name: 'Alice' }, 5 // 如果 mapInstance 很大,转为 Proxy 会很慢且没必要 6 mapInstance: markRaw(new Map()) 7});
4. 列表渲染优化:key 的正确使用
- 原理:Vue 使用虚拟 DOM Diff 算法。
key帮助 Vue 识别哪些节点被添加、移除或移动。 - 最佳实践:
- ✅ 必须使用唯一且稳定的 ID(如数据库 ID)。
- ❌ 避免使用索引 (
index) 作为 key:如果列表顺序会改变(排序、删除、插入),使用索引会导致 Vue 复用错误的 DOM 节点,引发不必要的重渲染甚至状态错乱。
- 代码示例:
html预览
1<!-- ✅ 正确 --> 2<div v-for="item in items" :key="item.id">...</div> 3 4<!-- ❌ 错误 (如果 items 会排序或增删) --> 5<div v-for="(item, index) in items" :key="index">...</div>
5. 事件处理优化:v-once 的兄弟 @once 与 防抖/节流
虽然 Vue 没有内置
@once 修饰符用于所有场景(早期版本有过讨论,目前主要靠逻辑控制),但可以通过以下方式减少事件触发导致的重渲染:- 逻辑控制:在事件处理函数内部添加标志位,防止重复提交或频繁触发。
- 工具库:使用
lodash.debounce或lodash.throttle包装事件处理函数,减少高频事件(如scroll,resize,input)触发的更新频率。
6. 组件层级优化
- 拆分组件:将大组件拆分为小组件。父组件的状态变化不会导致子组件重渲染,除非子组件接收的
props发生了变化。 v-memo(Vue 3.2+):- 类似于
v-once,但可以指定依赖数组。只有当依赖数组中的值发生变化时,模板的一部分才会被更新。 - 适用场景:极其昂贵的列表项渲染,且只有特定 prop 变化时才需要重绘。
html预览1<div v-for="item in list" :key="item.id" v-memo="[item.id, item.selected]"> 2 <!-- 只有 id 或 selected 变化时,这块 DOM 才会 diff/更新 --> 3 <complex-component :data="item" /> 4</div>- 类似于
7. 避免常见的“陷阱”
- 不要在模板中定义对象/数组:
html预览
1<!-- ❌ 每次渲染都会创建新对象,导致子组件认为 props 变了而重渲染 --> 2<child :config="{ theme: 'dark', size: 'large' }" /> 3 4<!-- ✅ 使用 computed 或常量 --> 5<child :config="staticConfig" /> - 慎用
v-ifvsv-show:- 如果需要频繁切换显示状态,用
v-show(只改 CSSdisplay,不销毁重建 DOM)。 - 如果初始化时条件为假且很少变为真,用
v-if(减少初始渲染开销)。
- 如果需要频繁切换显示状态,用
总结建议表
表格
| 场景 | 推荐方案 | 核心作用 |
|---|---|---|
| 纯静态内容 | v-once |
彻底跳过更新检查 |
| 复杂逻辑/衍生数据 | computed |
依赖缓存,避免重复计算 |
| 超大对象/外部实例 | shallowRef / markRaw |
减少 Proxy 开销,避免深层监听 |
| 列表渲染 | 稳定的 :key |
优化 Diff 算法,减少 DOM 操作 |
| 昂贵子树渲染 | v-memo |
细粒度控制模板更新 |
| 频繁切换显隐 | v-show |
避免 DOM 销毁和重建 |
通过组合使用这些策略,可以显著降低 Vue 应用的运行时开销,特别是在处理大量数据或复杂交互的场景下。