Meta Description:本文从响应式系统、组件渲染、状态设计、列表优化、异步组件、KeepAlive、打包拆分等角度,系统总结 Vue3 项目中的性能优化实践,适合中大型前端项目参考。
Slug:vue3-performance-optimization-practices
前言
Vue3 本身性能已经非常优秀。
相比 Vue2,Vue3 在响应式系统、编译优化、Tree Shaking、组件更新机制等方面都做了大量改进。
但是,这并不意味着使用 Vue3 就一定能写出高性能项目。
在真实业务中,很多 Vue3 项目仍然会出现:
页面首次加载慢
列表滚动卡顿
表单输入延迟
组件重复渲染
状态变化影响范围过大
构建产物体积过大
这些问题通常不是 Vue3 不够快,而是项目设计方式不合理。
性能优化不是后期补救,而应该贯穿在组件设计、状态管理、数据结构、路由拆分和工程化配置中。
本文将从实际项目角度,总结 Vue3 中最值得关注的性能优化方向。
先理解 Vue3 的响应式成本
Vue3 使用 Proxy 实现响应式。
相比 Vue2 的 Object.defineProperty,Proxy 可以更完整地监听对象读取、修改、删除、遍历等操作。
但响应式并不是免费的。
当一个对象被 reactive 包裹后,Vue 需要在读取时收集依赖,在修改时触发更新。
如果你把一个非常大的对象直接做成响应式,例如:
一个包含几千条数据的表格
一个深层嵌套的配置对象
一个后端返回的巨大 JSON
那么 Vue 会为这些对象建立响应式追踪。
如果这些数据并不需要被频繁修改,这种响应式成本就是浪费。
因此,Vue3 性能优化的第一原则是:
不是所有数据都应该变成响应式。
ref、reactive、shallowRef 如何选择?
很多开发者在 Vue3 中习惯性使用 reactive。
但在大型项目中,应该更谨慎。
如果是简单值,例如字符串、数字、布尔值,优先使用 ref。
如果是页面表单对象,可以使用 reactive。
如果是大型对象,并且只关心整体替换,不关心深层属性变化,可以使用 shallowRef。
例如,后端返回了一整份图表配置,你只需要整体更新,不需要追踪内部每个字段,就更适合 shallowRef。
shallowRef 的优势是:
只追踪 value 本身变化,不递归追踪内部结构。
这在图表、地图、编辑器实例、大型 JSON 数据中非常有用。
markRaw 的使用场景
Vue3 提供了 markRaw,用来标记一个对象永远不需要响应式。
适合场景包括:
第三方库实例
地图对象
富文本编辑器实例
图表实例
大型静态配置
例如 ECharts 实例、Monaco Editor 实例、Mapbox 地图对象,这些对象本身内部已经有复杂状态管理,不应该再被 Vue 转成响应式对象。
如果不小心把这些对象放进 reactive,可能会带来性能问题,甚至引发异常。
因此,在 Vue3 项目中,遇到第三方库实例时,应该优先考虑 markRaw。
computed 不只是语法糖
很多人把 computed 当成语法糖。
实际上,computed 的核心价值是缓存。
如果一个数据由其他状态计算得来,并且计算逻辑稍微复杂,就应该使用 computed,而不是在模板中直接写复杂表达式。
例如:
不要在模板里频繁做 filter、map、sort。
因为模板每次渲染时,这些表达式都有可能重新执行。
更好的做法是:
把复杂计算放到 computed 中。
computed 只有在依赖变化时才会重新计算。
这对列表过滤、权限判断、菜单生成、统计数据计算等场景非常重要。
watch 不要滥用
watch 非常强大,但也非常容易滥用。
很多项目中会出现大量 watch:
watch 某个表单字段
watch 某个对象
watch 某个路由参数
watch 某个 store 状态
如果 watch 逻辑过多,项目会变得很难追踪。
更糟糕的是,deep watch 会递归监听整个对象。
如果对象很大,性能开销会非常明显。
建议:
能用 computed 解决的,不用 watch。
只监听真正需要副作用的状态。
避免对大对象使用 deep watch。
watch 中不要写过于复杂的业务逻辑。
如果 watch 逻辑越来越复杂,通常说明状态设计本身有问题。
组件拆分不是越细越好
组件化是 Vue 的核心思想。
但组件拆分也不是越细越好。
过度拆分会带来:
组件层级过深
Props 传递复杂
事件冒泡混乱
调试困难
额外渲染成本
合理的组件拆分应该围绕业务边界,而不是机械地把每一小块 UI 都拆出去。
例如,一个用户管理页面可以拆成:
UserSearchForm
UserTable
UserEditDialog
UserPermissionPanel
这种拆分是合理的。
但如果把每一个标题、每一个按钮、每一个小图标都拆成业务组件,反而会增加维护成本。
组件拆分的标准应该是:
是否可以复用?
是否有独立逻辑?
是否能降低复杂度?
如果拆分后反而更难理解,就没有必要拆。
v-if 和 v-show 如何选择?
v-if 和 v-show 是 Vue 中非常常见的性能细节。
v-if 是条件渲染。
条件为 false 时,组件不会创建。
v-show 是显示隐藏。
组件始终存在,只是通过 CSS 控制 display。
如果一个模块很少显示,例如弹窗、权限面板、详情抽屉,使用 v-if 更合适。
如果一个模块需要频繁切换,例如 Tab 内容、展开收起、菜单切换,使用 v-show 更合适。
错误选择会影响性能。
频繁切换的组件如果使用 v-if,会反复创建和销毁。
复杂组件如果一直用 v-show,即使用户看不到,也会长期占用资源。
大列表必须使用虚拟滚动
如果页面需要渲染几千条甚至上万条数据,不能直接 v-for 全部渲染。
DOM 数量过多会导致:
页面卡顿
滚动不流畅
内存占用高
首次渲染慢
这种场景必须使用虚拟滚动。
虚拟滚动的核心思想是:
页面上只渲染用户当前可见区域的数据。
看起来像是完整列表,实际上 DOM 数量始终很少。
适合场景包括:
日志列表
消息列表
表格数据
选择器大数据
树形结构
如果后台管理系统中存在大表格,虚拟滚动通常是最有效的优化手段之一。
表格性能优化
企业后台项目中,Table 往往是性能问题高发区。
常见问题包括:
列太多
行太多
每个单元格都有复杂组件
大量 formatter
频繁更新 dataSource
表格内嵌表单
优化建议:
分页加载,不要一次返回所有数据。
大数据使用虚拟表格。
避免在单元格中写复杂计算。
formatter 结果尽量缓存。
不要频繁替换整个数组。
复杂弹窗不要放在每一行内部。
很多表格卡顿并不是 UI 库的问题,而是业务写法导致每一行都承担了过多逻辑。
路由级代码拆分
大型 Vue3 项目必须做路由懒加载。
用户访问首页时,不应该加载整个系统所有页面的代码。
例如:
订单模块
用户模块
报表模块
系统设置
这些都可以按路由拆分。
这样首屏只加载当前页面需要的资源。
其他模块等用户访问时再加载。
这对后台系统、SaaS、管理平台非常重要。
尤其是一个项目有几十个页面时,路由懒加载几乎是必做项。
defineAsyncComponent 的使用
除了路由懒加载,组件本身也可以异步加载。
适合场景包括:
大型弹窗
富文本编辑器
图表组件
代码编辑器
地图组件
低频使用的复杂业务组件
例如,一个页面中有一个“高级配置”弹窗,用户很少点击。
那么这个弹窗没有必要在页面首次加载时就打包进去。
可以使用异步组件,等用户真正打开时再加载。
这样可以显著减少首屏体积。
KeepAlive 不要无限缓存
KeepAlive 可以缓存组件状态,避免组件反复销毁和重建。
它非常适合:
Tab 页面
列表页返回详情后保留筛选条件
多页签后台系统
但是,KeepAlive 不是越多越好。
如果无限缓存页面,内存会不断增长。
尤其是后台系统中,用户打开几十个页面后,如果全部缓存,浏览器内存会明显增加。
建议:
只缓存真正需要保留状态的页面。
设置合理的 include 和 exclude。
在合适时机清理缓存。
对大型页面谨慎使用 KeepAlive。
Pinia 中的性能问题
Pinia 本身很轻量,但也可能被错误使用。
常见问题:
把所有接口数据都放进 Store。
把页面局部状态放进 Store。
一个 Store 过于庞大。
多个 Store 互相依赖。
Store 中保存大型响应式对象。
Pinia 更适合保存跨页面共享状态。
例如:
用户信息
权限
系统配置
主题
语言
不要把每个页面的搜索结果、表格数据、弹窗状态都放进 Store。
否则 Store 会越来越大,响应式依赖也会越来越复杂。
避免无意义的响应式传递
在组件之间传递数据时,不要把整个大对象传给子组件。
例如父组件有一个 user 对象,子组件只需要 user.name。
如果直接传整个 user,后续 user 其他字段变化,也可能影响子组件。
更好的做法是:
只传子组件真正需要的数据。
这样组件依赖更明确,更新范围也更小。
这类优化在大型表单、复杂表格、嵌套组件中非常有价值。
打包体积优化
Vue3 项目通常使用 Vite。
但使用 Vite 不代表构建产物一定小。
仍然需要关注:
第三方依赖体积
是否引入完整 UI 库
是否引入完整工具库
是否合理拆分 chunk
是否开启压缩
图片是否过大
常见问题是:
只用 lodash 的一个函数,却引入整个 lodash。
只用图标库几个图标,却引入整套图标。
只用 ECharts 一部分能力,却引入全部模块。
这些都会导致 bundle 体积变大。
建议定期使用可视化工具分析构建产物。
性能优化不要只看开发环境
很多开发者觉得页面不卡,就认为性能没问题。
但开发环境和生产环境差异很大。
开发环境没有完整压缩。
生产环境有缓存和 CDN。
本地电脑性能很强。
真实用户设备可能很弱。
因此,性能测试应该尽量接近真实环境。
尤其要关注:
低端设备
移动端
弱网络
首次访问
无缓存访问
只有这样,优化才有实际意义。
总结
Vue3 性能优化不是某一个技巧,而是一整套工程实践。
它涉及:
响应式设计
组件拆分
列表渲染
状态管理
路由拆分
异步组件
缓存策略
打包优化
真正优秀的 Vue3 项目,不是后期发现卡顿再优化,而是在架构设计阶段就避免不必要的性能成本。
记住一个原则:
Vue3 已经足够快,但错误的写法仍然可以让项目变慢。
性能优化的关键,不是记住所有 API,而是理解数据变化如何影响组件更新,理解哪些数据需要响应式,哪些组件需要懒加载,哪些状态应该被缓存,哪些资源应该被拆分。
只有这样,才能写出真正适合长期维护的大型 Vue3 项目。
X记录空间