性能优化的核心不是盲目使用 API,而是先确认瓶颈,再减少不必要的加载、响应式追踪、组件更新和 DOM 操作。
不要看到页面卡顿就直接加 v-memo 或虚拟列表。先区分问题发生在哪个阶段:
| 现象 | 优先检查 |
|---|---|
| 首屏白屏或可交互时间很长 | JavaScript 体积、网络请求、路由和组件懒加载 |
| 数据变化后页面更新慢 | 响应式范围、组件更新边界、列表规模 |
| 滚动卡顿 | 单次渲染节点数量、滚动事件、布局和绘制 |
| 切换页面越来越慢或内存增长 | 定时器、事件监听、订阅和组件缓存 |
常用手段包括:浏览器 Performance 面板、Vue Devtools 的组件更新检查、构建产物分析,以及记录优化前后的首屏时间、交互延迟和内存占用。优化后必须用同一场景复测,避免把主观感受当成结论。
通过动态 import() 将不属于首屏的页面拆成独立 chunk,访问时再加载:
对于首屏不会出现的重量级组件,可以使用 defineAsyncComponent。应同时考虑 loading、error、超时和重试状态,避免网络失败时页面一直空白。异步组件的详细用法见异步组件。
懒加载会增加请求和加载状态管理成本。首屏必需内容不应为了拆包而过度延迟。
让组件只接收它真正需要的数据,避免把会频繁变化的整个大对象传给大量子组件。可以把高频变化区域拆成更小的组件,使更新范围更明确。
相比传入整个 user 对象,稳定、精确的 props 更容易让子组件跳过无关更新。需要注意:不要为了追求“稳定”而复制大量对象或增加复杂的计算,最终仍应以测量结果为准。
对于依赖响应式数据的派生结果,使用 computed 可以缓存结果,并且只在依赖变化时重新计算;简单的一次性表达式不必强行抽成 computed。
computed 也不是万能缓存:如果每次都创建新的大对象,或依赖本身频繁变化,缓存收益会很有限。
大数据、第三方实例或本身不需要逐字段响应式的数据,不必全部交给深层代理:
shallowRef 只追踪 .value 的替换,适合整体替换的数据或实例;markRaw 适合第三方库实例、不可代理对象和不需要响应式的数据;keyv-for 应使用稳定且唯一的业务 ID:
不要默认使用数组下标。列表发生插入、删除或排序时,index 可能导致组件状态和 DOM 节点错误复用。只有列表永远不增删、不排序且没有内部状态时,才可以考虑使用下标。
当列表包含成千上万条数据时,即使 Diff 很快,创建和维护大量真实 DOM 仍然昂贵。虚拟列表只渲染可视区域附近的项目,适合高度固定或可以估算高度的长列表。
使用虚拟列表前应处理好:
v-memo 与v-oncev-memo 适合依赖明确、更新频率低的大型子树;依赖没有变化时可以跳过子树更新:
v-once 适合真正只需要渲染一次的内容。两者都应在确认存在更新瓶颈后使用,否则会增加理解和维护成本。Patch Flag、静态提升和 Block Tree 等编译时优化见Diff 算法。
v-if 和v-showv-if:条件为假时不创建 DOM,适合不常显示或创建成本较高的内容;v-show:始终创建 DOM,只切换 display,适合频繁切换且内容较轻的区域。选择时要同时考虑初始渲染成本、切换频率和隐藏内容是否占用资源,不能简单归纳为“频繁切换永远使用 v-show”。
高频滚动、拖拽和输入事件应避免在每次触发时执行昂贵逻辑,必要时使用节流、合并更新或把计算移出事件回调。事件监听、定时器和订阅必须在组件卸载时清理:
请求也要关注竞态:组件已经卸载或查询条件已经变化时,旧请求的结果不应覆盖新结果。可以使用 AbortController 或请求序列号取消、忽略过期响应。
KeepAlive 能缓存组件实例,减少反复创建的成本,但也会保留状态和资源。应配合 include、exclude 或 max 控制范围,并在 deactivated 时暂停不必要的轮询和监听。详见Keep-Alive。
computed,不是所有列表都需要虚拟化;shallowRef 和 markRaw 会改变响应式语义,不能只为“更快”而全局使用;nextTick 只保证 DOM 更新批次已经刷新,不会让昂贵计算自动变快;v-memo、v-once 可能让界面显示旧数据,使用前要确认依赖完整;Vue 性能优化可以按下面的顺序思考:
一句话总结:先让不需要加载的内容不加载,让不需要更新的组件不更新,让不需要存在的 DOM 不存在,最后用数据验证优化是否有效。