Vue 性能优化
大白话解释: Vue 性能优化就像"让车跑得更快"。有几种方式:
- 减少渲染次数:不该更新的组件不要更新
- 减少渲染内容:只渲染用户看到的部分
- 缓存组件:切换页面时不销毁,保留状态
- 减小打包体积:只打包用到的代码
为什么要性能优化?
- 用户体验:页面加载慢、操作卡顿,用户会流失
- SEO 排名:加载速度影响搜索引擎排名
- 资源消耗:减少服务器和用户设备的负担
四个层面的优化:
- 组件层面:v-once、v-memo、shallowRef、KeepAlive
- 渲染层面:虚拟滚动、懒加载、按需引入
- 状态层面:合理使用 computed、避免不必要的响应式
- 构建层面:代码分割、Tree Shaking、CDN
Vue 3 项目中常用的性能优化手段,从组件、渲染、状态、构建四个层面总结。
一、组件层面
v-once —— 静态内容缓存
vue
<!-- 渲染一次后不再更新,Vue 会跳过该节点的 diff -->
<p v-once>这段文字永远不会变:{{ staticText }}</p>
<!-- 整个组件树只渲染一次 -->
<div v-once>
<h1>{{ title }}</h1>
<p>{{ description }}</p>
</div>💡 适合纯静态内容,如文章正文、协议条款等。
v-memo —— 条件缓存(Vue 3.2+)
vue
<template>
<!-- 只有 item.id === selected 或 item.updated 变化时才重新渲染 -->
<div
v-for="item in list"
:key="item.id"
v-memo="[item.id === selected, item.updated]"
>
<p>id: {{ item.id }}</p>
<p>是否选中: {{ item.id === selected }}</p>
<p>更新时间: {{ item.updated }}</p>
</div>
</template>💡 大列表中性能提升显著。v-memo 数组中的值都不变时,整个 vnode 会被跳过。
shallowRef / shallowReactive
ts
import { shallowRef, shallowReactive } from 'vue'
// 只有 .value 整体替换才触发更新
const hugeArray = Array.from({ length: 10000 }, (_, i) => ({ id: i, data: `item-${i}` }))
const bigData = shallowRef(hugeArray)
// 只有第一层是响应式的
const state = shallowReactive({
name: '张三', // ✅ 响应式
nested: { count: 0 }, // ❌ 不是响应式的
})💡 适合大型数据结构只关心引用变化的场景,如从接口返回的列表。
defineAsyncComponent —— 异步组件
ts
import { defineAsyncComponent } from 'vue'
// 基本懒加载
const HeavyComponent = defineAsyncComponent(() =>
import('./components/HeavyChart.vue')
)
// 带选项
const AdminPanel = defineAsyncComponent({
loader: () => import('./AdminPanel.vue'),
delay: 200, // 延迟显示 loading(避免闪烁)
timeout: 10000, // 超时时间
errorComponent: ErrorDisplay,
loadingComponent: LoadingSpinner,
// 失败重试
onError(error, retry, fail, attempts) {
if (attempts <= 3) retry()
else fail()
},
})KeepAlive —— 缓存组件
vue
<template>
<KeepAlive :max="10" include="PageA,PageB" exclude="PageC">
<component :is="currentComponent" />
</KeepAlive>
</template>vue
<script setup lang="ts">
import { onActivated, onDeactivated } from 'vue'
// 缓存激活时
onActivated(() => {
console.log('组件被激活')
// 可重新获取数据
})
// 进入缓存时
onDeactivated(() => {
console.log('组件被缓存')
// 清理副作用
})
</script>⚠️
max设置最大缓存数,避免内存膨胀。
组件拆分原则
vue
<!-- ❌ 单文件 2000 行,每次数据变化整个组件重新渲染 -->
<template>
<Header />
<Sidebar />
<Table :data="hugeList" />
<Chart :data="hugeList" />
<Footer />
</template>
<!-- ✅ 拆分为独立组件,只有数据变化的组件重新渲染 -->
<template>
<Header />
<Sidebar />
<DataTable :data="hugeList" /> <!-- 只有 list 变化时重新渲染 -->
<DataChart :data="chartData" /> <!-- 只有 chartData 变化时重新渲染 -->
<Footer /> <!-- 永远不重新渲染 -->
</template>二、渲染层面
虚拟列表
长列表使用虚拟滚动,只渲染可见区域,避免大量 DOM 节点拖慢性能。
bash
# 推荐库:@tanstack/vue-virtual
yarn add @tanstack/vue-virtualvue
<script setup lang="ts">
import { ref } from 'vue'
import { useVirtualizer } from '@tanstack/vue-virtual'
// 父容器 ref
const parentRef = ref<HTMLElement>()
// 模拟 10000 条数据
const items = Array.from({ length: 10000 }, (_, i) => `第 ${i} 条`)
// 创建虚拟化实例 —— 只渲染可见区域的行
const virtualizer = useVirtualizer({
count: items.length, // 总条数
getScrollElement: () => parentRef.value!, // 滚动容器
estimateSize: () => 35, // 每行预估高度(px)
overscan: 5, // 可见区域外多渲染的行数
})
</script>
<template>
<!-- 外层容器:固定高度 + overflow 自动滚动 -->
<div ref="parentRef" style="height: 400px; overflow: auto;">
<!-- 内层撑开总高度,让滚动条正确 -->
<div :style="{ height: virtualizer.getTotalSize() + 'px', position: 'relative' }">
<!-- 只渲染可见行 -->
<div
v-for="row in virtualizer.getVirtualItems()"
:key="row.key"
:style="{ position: 'absolute', top: row.start + 'px', width: '100%' }"
>
{{ items[row.index] }}
</div>
</div>
</div>
</template>💡 详细用法见 TanStack Virtual 文档。
v-if vs v-show
| 指令 | 初始渲染 | 切换开销 | 适用场景 |
|---|---|---|---|
| v-if | 懒加载,条件为 false 不渲染 | 高(销毁/重建) | 条件很少改变 |
| v-show | 始终渲染 | 低(CSS 切换) | 频繁切换 |
事件优化(防抖 / 节流)
vue
<script setup lang="ts">
import { useThrottleFn, useDebounceFn } from '@vueuse/core'
// 滚动节流 —— 每 200ms 最多执行一次
const onScroll = useThrottleFn(() => { /* ... */ }, 200)
// 搜索防抖 —— 停止输入 300ms 后执行
const onSearch = useDebounceFn((keyword: string) => { /* ... */ }, 300)
</script>避免不必要的渲染
vue
<script setup lang="ts">
import { computed } from 'vue'
// ❌ 每次渲染都创建新数组
// <div v-for="item in list.filter(i => i.active)">
// ✅ 用 computed 缓存
const activeItems = computed(() => list.value.filter((i) => i.active))
</script>
<template>
<div v-for="item in activeItems" :key="item.id">
{{ item.name }}
</div>
</template>三、状态层面
精确更新,避免大对象替换
ts
// ❌ 整个对象替换,触发所有依赖该对象的 watcher
state.user = { ...state.user, name: '新名字' }
// ✅ 只修改需要的属性
state.user.name = '新名字'computed 缓存
ts
// 假设 bigList 是一个 Ref<Array<{ name: string; active: boolean }>>
const bigList = ref(Array.from({ length: 10000 }, (_, i) => ({ name: `item-${i}`, active: i % 2 === 0 })))
// ❌ 每次访问都重新计算
function expensive() {
return bigList.value.filter(...).map(...).reduce(...)
}
// ✅ computed 有缓存,只在依赖变化时重新计算
const result = computed(() => {
return bigList.value.filter(...).map(...).reduce(...)
})避免深度监听
ts
// 假设 obj 的类型为 Ref<{ name: string; age: number }>
const obj = ref({ name: '张三', age: 25 })
// ❌ deep: true 性能差,会递归遍历对象所有层级
watch(obj, handler, { deep: true })
// ✅ 精确监听某个属性,只在 name 变化时触发
watch(() => obj.value.name, handler)四、构建层面
Vite 构建配置详见 Vue 3 + Vite 开发指南。
路由懒加载
ts
const routes = [
{
path: '/dashboard',
component: () => import('@/views/Dashboard.vue'), // 自动 code splitting
},
]组件库按需引入
ts
// vite.config.ts
import Components from 'unplugin-vue-components/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
export default {
plugins: [
Components({
resolvers: [NaiveUiResolver()],
}),
],
}分包策略
ts
// vite.config.ts — 将大依赖拆为独立 chunk
import { defineConfig } from 'vite'
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'echarts': ['echarts'],
},
},
},
},
})📖 图片优化、Gzip 压缩、CDN 加速等通用构建优化见 前端性能优化
五、Bundle 分析
bash
# 安装分析工具
yarn add -D rollup-plugin-visualizerts
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer'
export default {
plugins: [
visualizer({
open: true, // 构建后自动打开报告
filename: 'stats.html', // 输出文件名
gzipSize: true, // 显示 gzip 后大小
}),
],
}bash
# 构建并查看分析报告
yarn build
# 自动打开 stats.html,查看各模块体积占比💡 找出体积最大的模块,考虑按需引入、替换轻量库、或拆分为异步 chunk。
六、开发调试
ts
// 使用 onRenderTracked / onRenderTriggered 调试响应式
import { onRenderTracked, onRenderTriggered } from 'vue'
onRenderTracked((event) => {
console.log('tracked:', event.target, event.key)
})
onRenderTriggered((event) => {
console.log('triggered:', event.target, event.key, event.oldValue, event.newValue)
})ts
// Vue Devtools 性能分析
// 打开 Vue Devtools → Performance → Start recording七、性能优化清单
| 类别 | 优化手段 | 效果 |
|---|---|---|
| 组件 | 路由懒加载 | 减少首屏 JS 体积 |
| 组件 | 异步组件 defineAsyncComponent | 按需加载重型组件 |
| 组件 | KeepAlive 缓存 | 避免重复渲染 |
| 渲染 | 虚拟列表 | 大列表流畅滚动 |
| 渲染 | v-memo / v-once | 跳过静态节点 diff |
| 渲染 | 事件节流/防抖 | 减少高频触发 |
| 状态 | computed 缓存 | 避免重复计算 |
| 状态 | shallowRef | 减少深层代理开销 |
| 构建 | 分包策略 | 并行加载,缓存复用 |
| 构建 | 图片 WebP + 懒加载 | 减少资源体积 |
| 构建 | Gzip/Brotli 压缩 | 传输体积减小 60%+(需服务器配置) |
| 分析 | rollup-plugin-visualizer | 定位体积瓶颈 |
💡 Gzip/Brotli 压缩需要在 Web 服务器(如 Nginx)或 CDN 层配置,前端构建工具(如 vite-plugin-compression)仅负责生成
.gz/.br预压缩文件。KeepAlive 的include/exclude除逗号分隔字符串外,还支持RegExp类型。