运行时优化
减少不必要的渲染与响应式开销。构建侧见 build。
运行时优化旨在减少不必要的 DOM 操作、响应式开销和计算成本,从而提升应用的交互流畅度和响应速度。以下是 Vue 3 中关键的运行时优化策略。
一、列表渲染优化
1. v-for 与 v-if 共用的冲突与解决方案
在同一个元素上同时使用 v-for 和 v-if 时,Vue 会先执行 v-for 再执行 v-if(因为 v-for 优先级更高),这意味着即使只有少数项需要渲染,也会遍历整个列表,造成性能浪费。Vue 官方不推荐这种写法,并会在开发模式下给出警告。
❌ 错误示例:
<li v-for="user in users" v-if="user.isActive" :key="user.id">{{ user.name }}</li>
✅ 解决方案:使用计算属性过滤
<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-if 和 v-for,但不推荐。
2. key 的作用机制与使用原则
key 是 Vue 虚拟 DOM 算法中用于标识节点复用的重要属性。当列表重新排序、插入或删除时,正确的 key 可以最大化复用已有 DOM 元素,减少不必要的 DOM 操作。
- 使用唯一 ID(如数据库主键、
uuid):<li v-for="item in list" :key="item.id"> - 避免使用数组索引作为
key:当列表顺序变化或中间插入/删除元素时,索引会导致错误的节点复用,引发状态错乱和性能下降。
<!-- ❌ 坏:用索引作为 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):每次重新渲染都会执行,没有缓存。适合需要副作用或每次调用都需要最新值的操作(如时间格式化、随机数)。
// 计算属性:只会在 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: translateY或padding模拟完整列表的滚动高度。
Vue 生态常用库:
vue-virtual-scroller(官方推荐)vue3-virtual-scroll-listtanstack-virtual(框架无关)
使用示例(vue-virtual-scroller):
<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是对象,对象内部变化不会触发更新。
import { shallowReactive } from "vue";
const bigData = shallowReactive({
meta: { version: 1 }, // meta 内部变化不会触发更新
items: [
/* 大量数据 */
], // items 内部变化不会触发更新
});
// 只有替换整个 bigData.meta 或 bigData.items 才会触发更新
适用场景:展示静态配置、不需要响应式深层的第三方实例、可整体替换的大数据集。
注意:一旦使用浅响应式,修改深层属性不会导致视图更新,需谨慎或配合 triggerRef 强制更新。
2. markRaw 标记永久不可响应化的数据
markRaw 标记的对象或值将永远不会被代理,即使传入 reactive 或 ref 也会保持原始状态。
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)的性能代价
当对一个大型深层对象使用 watch 的 deep: true 时,Vue 需要递归遍历对象的所有属性以检测变化。如果对象层级深、体积大,这会造成显著的 CPU 开销。
// ❌ 避免在大型对象上使用 deep
watch(
() => state.bigObject,
(newVal) => {
// 任何深层属性变化都会触发
},
{ deep: true },
);
更好的做法:
- 监听特定路径:使用
watch(() => state.bigObject.someField, ...) - 使用计算属性派生需要的值:如果只需要某个计算结果,使用
computed可自动缓存。 - 手动浅比较:对于嵌套对象,可以在需要的地方手动
triggerRef。
2. 优先使用计算属性替代深度监听
许多场景下,我们其实并不需要监听整个对象的变化,而是需要基于对象派生某个状态。计算属性天然具备缓存和依赖追踪,比深度监听更高效。
// 坏:深度监听
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-for 与 v-if(用计算属性);使用唯一 key;优先计算属性而非方法;万级列表使用虚拟滚动 |
| 大对象处理 | shallowRef、shallowReactive、markRaw 避免深度响应式开销 |
| 侦听器优化 | 避免大对象深度监听;用计算属性取代深度 watch;明确监听目标路径 |
| 其他 | v-once、v-memo、异步组件、shallowRef 外部实例 |
运行时优化往往是细节的堆叠。可结合 Lighthouse 与 Chrome Performance 面板定位瓶颈。
参考文献
以下链接在编写时均可正常访问:
| 资料 | 说明 |
|---|---|
| Vue:列表渲染 | v-for 与 key |
| Vue:性能 | 官方建议 |
| Vue:v-memo | 记忆化 |
| web.dev:Web Vitals | 核心指标(英文) |
相关文章
资源优化
图片懒加载、按需引入与 Web Vitals 监控。见 HTML 图片、CSS 字体优化。
编译与打包优化
构建阶段的代码分割、Tree Shaking 与 SFC 编译优化。运行时见 runtime。
运行时优化
在 变更检测 策略正确的前提下,减少模板计算与 DOM 规模。涵盖 OnPush、@for track、纯管道、虚拟滚动、懒加载、构建体积分析与常见性能陷阱。
运行时优化
减少不必要的组件重渲染与昂贵计算。涵盖重渲染成因、memo/useMemo/useCallback、虚拟滚动、Web Worker、Profiler 分析、最佳实践与常见坑。构建侧见 build。
GridView 宫格加载渲染优化
系统首页 GridView 宫格模块接口耗时不高,但首次进入总耗时接近 22s。复盘耗时分层定位过程、代码层面的瓶颈(约 2000 行、100 个 tab 重复节点)、优化手段与最终指标对比。
新商家系统性能优化实践
商家系统新版本上线后,团队持续针对构建效率与用户体验做了一轮系统性优化。本文记录核心方案与落地结果,供后续版本复用。
Series
performance
3 / 3