《Vue3 从入门到大神21篇》Vue3 性能优化九大策略
前言
Vue3 本身已经很快了——Proxy 响应式、PatchFlag 编译优化、Tree Shaking……
但"框架快"不等于"你的项目快"。
在实际项目中,性能问题往往来自:
-
❌ 超大列表一次性渲染几千条
-
❌ 频繁更新的响应式数据触发了大量重新渲染
-
❌ 不必要的深层响应式代理拖慢初始化
-
❌ 图片和资源没有懒加载
这一篇,我们系统梳理 9 个 Vue3 性能优化的实战策略,从响应式层面 → 渲染层面 → 资源层面,层层递进。
一、响应式层面优化
策略 1:shallowRef / shallowReactive —— 跳过深层代理
问题场景
// 一个巨大的列表,不需要深层响应式
const bigList = reactive(largeData) // ❌ 每一项都被代理
优化方案
import { shallowRef } from 'vue'
// 只有 .value 本身是响应式的,内部数据不被代理
const bigList = shallowRef(largeData)
// 需要更新时,整体替换
bigList.value = newData
|
API |
响应式深度 |
适用场景 |
|---|---|---|
|
|
深层 |
需要细粒度更新的数据 |
|
|
仅顶层 |
大列表、不可变数据 |
|
|
仅第一层 |
配置对象、只读结构 |
📌 经验法则:
如果数据不需要逐项修改,就用 shallow 版本。
策略 2:markRaw —— 标记永不代理
import { markRaw } from 'vue'
const staticConfig = markRaw({
theme: 'dark',
version: '1.0.0'
})
// 即使放进 reactive,也不会被代理
const state = reactive({
config: staticConfig // ✅ 不会被代理
})
✅ 适合:第三方库实例、常量配置、大型静态数据
策略 3:toRefs 的替代方案 —— 只暴露需要的响应式属性
// ❌ 把整个 reactive 解构暴露
const state = reactive({ a: 1, b: 2, c: 3 })
return { ...toRefs(state) } // 全部变成 ref
// ✅ 只暴露需要的
return {
a: computed(() => state.a),
b: computed(() => state.b)
}
二、渲染层面优化
策略 4:v-memo —— 条件性跳过 Diff
<template>
<div v-memo="[dep1, dep2]">
<!-- 只有当 dep1 或 dep2 变化时才重新渲染 -->
<ExpensiveComponent :data="data" />
</div>
</template>
📌 与 computed 的区别:
-
computed缓存计算结果 -
v-memo缓存整个子树的渲染
⚠️ 不要滥用:只在确实昂贵的子树中使用
策略 5:虚拟列表(Virtual List)—— 长列表终极方案
问题
<!-- 渲染 10000 条数据 -->
<div v-for="item in 10000" :key="item">
{{ item }}
</div>
❌ DOM 节点过多,页面卡死
解决方案:vue-virtual-scroller / vueuc
<template>
<RecycleScroller
class="scroller"
:items="largeList"
:item-size="50"
key-field="id"
v-slot="{ item }"
>
<div class="row">
{{ item.name }}
</div>
</RecycleScroller>
</template>
<script setup>
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
</script>
✅ 只渲染可视区域内的元素,DOM 数量恒定
策略 6:v-for 与 v-if 的正确用法
<!-- ❌ 错误:v-if 和 v-for 在同一元素 -->
<div v-for="item in list" v-if="item.active" :key="item.id">
{{ item.name }}
</div>
<!-- ✅ 正确:先过滤,再循环 -->
<div v-for="item in activeList" :key="item.id">
{{ item.name }}
</div>
const activeList = computed(() =>
list.value.filter(item => item.active)
)
📌 Vue3 中 v-if 优先级高于 v-for,但混用仍然不推荐。
三、组件层面优化
策略 7:组件懒加载(defineAsyncComponent)
import { defineAsyncComponent } from 'vue'
// 路由级组件
const UserDetail = defineAsyncComponent(() =>
import('@/views/UserDetail.vue')
)
// 条件渲染的重型组件
const RichEditor = defineAsyncComponent(() =>
import('@/components/RichEditor.vue')
)
<template>
<button @click="showEditor = true">打开编辑器</button>
<RichEditor v-if="showEditor" />
</template>
✅ 只有需要时才会加载组件代码
策略 8:KeepAlive 缓存 + max 控制
<KeepAlive :max="10">
<component :is="currentTab" />
</KeepAlive>
✅ 前面第 16 篇详细讲过,这里不再赘述
四、资源层面优化
策略 9:图片懒加载
方案一:原生 loading 属性(最简单)
<img src="large-image.jpg" loading="lazy" alt="描述" />
✅ 浏览器原生支持,零依赖
方案二:Intersection Observer 自定义指令
// directives/lazy.ts
export const lazy = {
mounted(el: HTMLImageElement, binding: any) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
el.src = binding.value
observer.unobserve(el)
}
})
})
observer.observe(el)
}
}
<template>
<img v-lazy="'/images/photo.jpg'" alt="照片" />
</template>
五、性能优化 Checklist
|
类别 |
检查项 |
|---|---|
|
响应式 |
大对象用 |
|
渲染 |
长列表用虚拟列表 |
|
渲染 |
昂贵子树用 |
|
组件 |
重型组件用 |
|
资源 |
图片懒加载 |
|
构建 |
路由懒加载 |
|
构建 |
开启 Gzip / Brotli |
|
监控 |
使用 Vue DevTools Profiler |
六、性能分析工具
1️⃣ Vue DevTools Performance Tab
-
记录组件渲染时间
-
找出渲染瓶颈
2️⃣ Chrome DevTools Performance
-
录制页面操作
-
分析长任务和强制回流
3️⃣ Lighthouse
npm install -g lighthouse
lighthouse http://localhost:5173
七、常见优化误区
|
误区 |
真相 |
|---|---|
|
所有数据都用 shallowRef |
过度优化,增加维护成本 |
|
v-memo 到处用 |
反而增加内存开销 |
|
虚拟列表用于短列表 |
杀鸡用牛刀 |
|
过早优化 |
先测量,再优化 |
📌 Donald Knuth 的名言:
"过早优化是万恶之源。"
八、面试高频问答
Q1:shallowRef 和 ref 的区别?
shallowRef 只有 .value 是响应式的,内部数据不被代理。
Q2:v-memo 的使用场景?
渲染开销大的子树,且依赖变化不频繁的场景。
Q3:长列表优化的核心思路?
只渲染可视区域内的元素(虚拟列表)。
九、总结(实战版)
Vue3 性能优化不是"玄学",而是分层递进的系统工程:
-
响应式层:减少不必要的代理
-
渲染层:减少 DOM 数量和 Diff 开销
-
组件层:延迟加载和缓存
-
资源层:懒加载和压缩
📢 下期预告
👉 第 22 篇:渲染优化 —— PatchFlags 与 Tree Shaking 的黑盒解析
更多推荐



所有评论(0)