上周,我们上线了一个数据看板页面,本地跑得飞快,一上生产------滚动卡成 PPT。
Profiler 一抓,发现:
- 每次滚动都在重复创建
computed函数 - 列表项里嵌套了 3 层
<Suspense> - 一个
watch竟然监听了整个reactive对象......
问题不在逻辑,而在 "你以为没问题的写法"。
今天,我就分享 5 个 Vue 3 中少有人提、但效果惊人的性能优化技巧,尤其第 4 个,连很多 5 年经验的老手都没用过。
技巧 1:别在模板里写"方法调用",用 computed + 缓存
反面教材:
html
<template>
<div>{{ formatUserName(user) }}</div> <!-- 每次渲染都执行! -->
</template>
<script setup>
const formatUserName = (user) => `${user.firstName} ${user.lastName}`;
</script>
正确做法:
ts
const formattedName = computed(() =>
`${user.value.firstName} ${user.value.lastName}`
);
html
<template>
<div>{{ formattedName }}</div> <!-- 响应式缓存,依赖不变不重算 -->
</template>
关键点 :模板中的函数调用 没有缓存,每次 re-render 都会执行!
技巧 2:v-for 里的组件,记得加 key ------ 但别用 index!
很多人知道要加 key,但随手写:
html
<div v-for="(item, index) in list" :key="index">
<ItemCard :data="item" />
</div>
问题 :当列表发生插入/删除 时,index 会变,导致 Vue 错误复用组件实例,引发状态错乱 or 不必要的销毁重建。
正确做法:用唯一 ID
html
<div v-for="item in list" :key="item.id">
<ItemCard :data="item" />
</div>
如果真没 ID?考虑用
Symbol()或crypto.randomUUID()生成稳定 key(仅限静态列表)。
技巧 3:慎用 watch 监听整个 reactive 对象
ts
const state = reactive({ a: 1, b: 2, c: 3 });
watch(state, () => {
console.log('state changed');
});
这会导致:只要 a、b、c 任意一个变了,回调就触发 ,即使你只关心 a。
更精准的写法:
ts
// 方案 A:监听具体属性
watch(() => state.a, (newVal) => { ... });
// 方案 B:用 toRefs 解构后监听
const { a } = toRefs(state);
watch(a, (newVal) => { ... });
高级技巧:如果必须监听多个字段,用 getter 函数组合:
ts
watch(
() => ({ a: state.a, b: state.b }),
(newVals) => { /* 只有 a 或 b 变才触发 */ }
);
技巧 4:用 shallowRef 和 markRaw 跳过不必要的响应式(隐藏大招!)
这是 Vue 3 响应式系统中最被低估的 API。
场景:你有一个大型配置对象 or 第三方库实例(如 echarts 实例),不需要响应式?
默认写法(性能杀手):
ts
const chart = ref(null); // Vue 会尝试把 echarts 实例变成响应式!
onMounted(() => {
chart.value = echarts.init(dom); // 内部 thousands of properties!
});
正确做法:
ts
// 方案 A:用 shallowRef(只让 .value 响应,内部不递归)
const chart = shallowRef(null);
// 方案 B:用 markRaw 明确告诉 Vue "别动它"
const chartInstance = markRaw(echarts.init(dom));
const chart = ref(chartInstance);
效果:避免 Vue 递归遍历大型对象,节省内存 + 提升初始化速度 10x+
适用场景:
- 图表实例(ECharts、Chart.js)
- 复杂配置对象(如 Monaco Editor options)
- 不变的数据结构(如路由 meta、常量字典)
技巧 5:懒加载组件 + 异步 setup,减少首屏负担
别让所有组件都在首屏加载!
html
<!-- 同步引入,打包进主 chunk -->
<script setup>
import HeavyChart from './HeavyChart.vue';
</script>
改成动态导入 + Suspense:
html
<template>
<Suspense>
<template #default>
<LazyChart />
</template>
<template #fallback>
<div>Loading chart...</div>
</template>
</Suspense>
</template>
<script setup>
// 自动代码分割
const LazyChart = defineAsyncComponent(() => import('./HeavyChart.vue'));
</script>
进阶:配合
IntersectionObserver实现滚动到可视区再加载:
ts
const isVisible = ref(false);
// 当元素进入视口,isVisible = true → 再加载组件
总结:5 个技巧速查表
| 技巧 | 适用场景 | 性能收益 |
|---|---|---|
模板中用 computed 代替方法调用 |
频繁渲染的格式化逻辑 | 避免重复计算 |
v-for 用唯一 ID 做 key |
动态列表(增删改) | 减少 DOM 重建 |
精准 watch 而非监听整个对象 |
复杂状态管理 | 避免无效回调 |
shallowRef / markRaw 跳过响应式 |
大型对象、第三方实例 | 内存 & 初始化提速 |
| 异步组件 + Suspense | 重型组件(图表、编辑器) | 首屏加载更快 |
最后说两句
Vue 3 的性能,80% 取决于你如何使用响应式系统,而不是框架本身慢。
真正的优化,不是"加缓存""开 SSR",而是:
在正确的地方,用正确的 API,做最小化的响应式。
下次写组件前,先问自己:
"这个数据,真的需要响应式吗?"
各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!