Vue 性能优化

一、Vue 性能问题的本质

Vue 的响应式系统会自动追踪依赖,理论上只有真正依赖的数据变化时才会触发更新。但实践中常见的问题包括:模板中不合理的计算watch 过度触发列表渲染缺乏 key 以及 组件粒度过大

1
2
3
4
5
6
7
8
9
10
11
12
响应式数据变化

├── Vue 的优化优势
│ ├── 自动追踪依赖 → 不需要手动 memo
│ ├── 虚拟 DOM diff → 最小化 DOM 操作
│ └── 异步更新队列 → 批量合并更新

└── 常见性能陷阱
├── 模板中执行昂贵计算 → 每次渲染都执行
├── 列表没有 key → 全部重新创建
├── watch 监听大对象 → 深度监听开销高
└── 组件嵌套过深 → 大量 diff 开销

二、优化手段详解

计算属性 vs 方法——缓存计算结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- ❌ 方法:每次渲染都重新计算 -->
<template>
<div>{{ formatList(items) }}</div>
</template>
<script setup>
function formatList(items) {
return items.sort((a, b) => b.value - a.value).map(formatItem);
}
</script>

<!-- ✅ 计算属性:只有依赖变化时才重新计算 -->
<script setup>
import { computed } from 'vue';

const formattedList = computed(() => {
return props.items.sort((a, b) => b.value - a.value).map(formatItem);
});
</script>
<template>
<div>{{ formattedList }}</div>
</template>
1
2
3
├── computed → 有缓存,依赖不变就不重算 → ✅ 推荐
├── method → 无缓存,每次渲染都执行 → ❌ 昂贵计算避免使用
└── watch → 监听变化执行副作用 → 不要用于模板数据的衍生

v-memo——跳过不变列表项的重渲染

1
2
3
4
5
6
<!-- v-memo:依赖数组中的值未变化时,跳过该块的重渲染 -->
<template>
<div v-for="item in list" :key="item.id" v-memo="[item.id, item.updatedAt]">
<ListItem :item="item" />
</div>
</template>
1
2
3
4
适用场景:
├── 大列表中,只有少数项变化 → v-memo 跳过未变项
├── 配合虚拟滚动 → 进一步减少 diff 工作量
└── 列表项包含复杂子组件 → 避免深层 diff

v-once / v-memo——稳定内容只渲染一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- v-once:只在首次渲染,不再更新 -->
<template>
<div v-once>
<h1>固定的标题</h1>
<p>不会变化的内容</p>
</div>
</template>

<!-- v-memo="[]":空依赖表示永不更新 -->
<template>
<div v-memo="[]">
<UserInfo :user="user" />
</div>
</template>

函数式组件——轻量无状态组件

1
2
3
4
5
6
<!-- 函数式组件:没有实例、没有 this、开销极小 -->
<template functional>
<div class="cell">{{ props.value }}</div>
</template>

<!-- Vue 3 中推荐用普通组件,因为编译优化已使其接近函数式组件的性能 -->

组件拆分——控制更新粒度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- ❌ 单个组件包含大量不相关的状态 -->
<template>
<div>
<input v-model="search" placeholder="搜索" /> <!-- 搜索输入 -->
<div v-for="item in filteredList">...</div> <!-- 列表渲染 -->
<aside>
<UserProfile /> <!-- 用户信息 -->
<NotificationList /> <!-- 通知列表 -->
</aside>
</div>
</template>

<!-- ✅ 拆分为独立组件,各自追踪自己的响应式依赖 -->
<template>
<div>
<SearchPanel />
<ItemList :items="items" />
<SidePanel />
</div>
</template>
1
2
3
4
原则:
├── 每个组件只关注自己的数据 → 数据变化时只有该组件更新
├── 不要在一个组件里混合频繁变化和几乎不变的内容
└── 状态尽量下放到叶子组件

列表与 Key

1
2
3
4
5
<!-- ❌ key 使用 index → 删除/插入时所有列表项重新创建 -->
<div v-for="(item, index) in items" :key="index">

<!-- ✅ key 使用唯一 id → Vue 正确复用 DOM -->
<div v-for="item in items" :key="item.id">

三、虚拟滚动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<template>
<div ref="container" class="virtual-list" @scroll="onScroll">
<div class="scroll-bar" :style="{ height: totalHeight + 'px' }">
<div
class="visible-items"
:style="{ transform: `translateY(${offsetY}px)` }"
>
<div
v-for="item in visibleItems"
:key="item.id"
class="list-item"
>
{{ item.name }}
</div>
</div>
</div>
</div>
</template>

<script setup>
import { ref, computed } from 'vue';

const props = defineProps({ items: Array, itemHeight: { type: Number, default: 50 } });
const container = ref(null);
const scrollTop = ref(0);
const containerHeight = ref(600);

const totalHeight = computed(() => props.items.length * props.itemHeight);

const visibleCount = computed(() => Math.ceil(containerHeight.value / props.itemHeight) + 2);
const startIndex = computed(() => Math.floor(scrollTop.value / props.itemHeight));

const visibleItems = computed(() => {
return props.items.slice(startIndex.value, startIndex.value + visibleCount.value);
});

const offsetY = computed(() => startIndex.value * props.itemHeight);

function onScroll() {
scrollTop.value = container.value.scrollTop;
}
</script>

社区推荐库:

Vue 版本特点
vue-virtual-scrollerVue 2/3功能全面,支持动态高度
vueuc (VUse)Vue 3Naive UI 同作者,轻量
TanStack Virtual框架无关最灵活

四、watch 优化

避免深度监听大对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- ❌ 深度监听大对象 → 每次变更都要遍历整个对象 -->
<script setup>
watch(
() => props.data,
(val) => { /* 处理 */ },
{ deep: true }
);
</script>

<!-- ✅ 明确指定监听的路径 -->
<script setup>
watch(
() => props.data.id,
(id) => { /* 只在 id 变化时触发 */ }
);
</script>

避免 watch 中执行昂贵操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!-- ❌ watch 中直接执行耗时操作 -->
<script setup>
watch(keyword, async (val) => {
results.value = await fetch(`/api/search?q=${val}`);
});
</script>

<!-- ✅ 防抖 + 取消上次请求 -->
<script setup>
import { ref, watch } from 'vue';

let abortController = null;

watch(keyword, (val) => {
if (abortController) abortController.abort();
abortController = new AbortController();

const timer = setTimeout(async () => {
results.value = await fetch(`/api/search?q=${val}`, {
signal: abortController.signal,
}).then(r => r.json());
}, 300);

// 下次 watch 触发时清除定时器
watch(keyword, () => clearTimeout(timer), { once: true });
});
</script>

五、Immutable 数据模式

Vue 的响应式系统基于 Proxy,不需要像 React 那样手动保证不可变。但过度使用 reactive 包裹大对象会带来额外开销:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- ✅ 对于大型静态数据,用 ref 或普通变量 -->
<script setup>
import { ref } from 'vue';

// ✅ 大数组用 ref 包裹整体
const items = ref([]);

// ✅ 更新时整体替换,而非逐个修改
function updateItems(newData) {
items.value = newData; // 整体替换,Vue 只追踪引用变化
}
</script>

六、编译优化(Vue 3)

Vue 3 在编译层面做了大量优化,通常不需要手动干预:

优化说明
静态提升不变的模板内容提升到 render 函数之外,只创建一次
补丁标记编译时标记动态节点,diff 时只检查动态部分
缓存事件内联事件处理函数自动缓存
树结构打平减少递归遍历的深度
1
2
3
4
5
Vue 3 默认优化:
├── 模板中静态内容 → 只创建一次,后续跳过
├── 动态绑定(:class / :style / v-if)→ 精确标记,只 diff 变化
├── 事件监听 → 自动缓存 handler 引用
└── v-for + v-if → 编译警告(优先用 computed 过滤)

七、排查手段

Vue DevTools

1
2
3
4
5
操作步骤:
1. 安装 Vue DevTools 浏览器扩展
2. 打开 DevTools → Vue 面板
3. Components → 查看组件树和更新频率
4. Performance → 录制操作,查看渲染时间
1
2
3
4
5
排查问题:
├── 频繁更新的组件高亮 → 是否更新了不必要的数据?
├── 组件渲染耗时过高 → 是否模板中有昂贵计算?
├── 大量组件同时更新 → 是否状态提升过高?
└── Timeline 中渲染频率过高 → 是否 watch 未加防抖?

组件渲染计数

1
2
3
4
5
6
7
8
9
10
11
<script setup>
// 开发阶段监控组件渲染次数
const renderCount = ref(0);

onBeforeUpdate(() => {
renderCount.value++;
if (renderCount.value > 5) {
console.warn('组件渲染次数过多,请检查优化');
}
});
</script>

检查 watcher 数量

1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup>
import { onMounted, getCurrentInstance } from 'vue';

onMounted(() => {
const instance = getCurrentInstance();
const watchers = instance?.proxy?.$forceUpdate
? '请使用 Vue DevTools 查看'
: instance?.proxy;

// 查看当前组件的 watcher 数量
console.log('组件内部 watcher:', Object.keys(instance?.proxy || {}).length);
});
</script>

八、排查清单

现象排查手段常用方案
输入框输入卡顿Vue DevTools Timeline拆分组件、computed
列表滚动卡顿渲染计数虚拟滚动、v-memo
大量列表渲染慢Performance 火焰图虚拟滚动、唯一 key
页面切换白屏Network 瀑布图defineAsyncComponent
watch 频繁触发检查依赖路径明确指定字段、防抖
模板中复杂计算Vue DevToolscomputed 替代方法
组件更新范围过大组件树检查拆分组件、状态下移