Vue 性能优化

性能优化的核心不是盲目使用 API,而是先确认瓶颈,再减少不必要的加载、响应式追踪、组件更新和 DOM 操作。

一、先定位瓶颈

不要看到页面卡顿就直接加 v-memo 或虚拟列表。先区分问题发生在哪个阶段:

现象优先检查
首屏白屏或可交互时间很长JavaScript 体积、网络请求、路由和组件懒加载
数据变化后页面更新慢响应式范围、组件更新边界、列表规模
滚动卡顿单次渲染节点数量、滚动事件、布局和绘制
切换页面越来越慢或内存增长定时器、事件监听、订阅和组件缓存

常用手段包括:浏览器 Performance 面板、Vue Devtools 的组件更新检查、构建产物分析,以及记录优化前后的首屏时间、交互延迟和内存占用。优化后必须用同一场景复测,避免把主观感受当成结论。

二、减少首屏加载成本

路由和组件懒加载

通过动态 import() 将不属于首屏的页面拆成独立 chunk,访问时再加载:

const routes = [
  {
    path: '/settings',
    component: () => import('./views/Settings.vue'),
  },
]

对于首屏不会出现的重量级组件,可以使用 defineAsyncComponent。应同时考虑 loading、error、超时和重试状态,避免网络失败时页面一直空白。异步组件的详细用法见异步组件

控制产物体积

  • 优先使用支持 Tree Shaking 的 ES Module 导入方式;
  • 避免在首屏引入编辑器、图表库等重量级依赖;
  • 对确实需要的大依赖做按页面或功能拆分;
  • 用构建产物分析确认体积主要来自哪里,而不是只看源码大小。

懒加载会增加请求和加载状态管理成本。首屏必需内容不应为了拆包而过度延迟。

三、减少不必要的组件更新

设计稳定的组件边界

让组件只接收它真正需要的数据,避免把会频繁变化的整个大对象传给大量子组件。可以把高频变化区域拆成更小的组件,使更新范围更明确。

<UserRow
  v-for="user in users"
  :key="user.id"
  :id="user.id"
  :name="user.name"
  :active="user.id === activeId"
/>

相比传入整个 user 对象,稳定、精确的 props 更容易让子组件跳过无关更新。需要注意:不要为了追求“稳定”而复制大量对象或增加复杂的计算,最终仍应以测量结果为准。

合理使用计算属性

对于依赖响应式数据的派生结果,使用 computed 可以缓存结果,并且只在依赖变化时重新计算;简单的一次性表达式不必强行抽成 computed

const activeUsers = computed(() =>
  users.value.filter((user) => user.active),
)

computed 也不是万能缓存:如果每次都创建新的大对象,或依赖本身频繁变化,缓存收益会很有限。

避免不必要的深层响应式

大数据、第三方实例或本身不需要逐字段响应式的数据,不必全部交给深层代理:

const chart = shallowRef(null)
const chartInstance = markRaw(createChart())
  • shallowRef 只追踪 .value 的替换,适合整体替换的数据或实例;
  • markRaw 适合第三方库实例、不可代理对象和不需要响应式的数据;
  • 使用浅层 API 后,深层修改不会自动触发更新,必须明确替换引用或手动触发。

四、列表和渲染优化

正确使用key

v-for 应使用稳定且唯一的业务 ID:

<li v-for="item in items" :key="item.id">
  {{ item.name }}
</li>

不要默认使用数组下标。列表发生插入、删除或排序时,index 可能导致组件状态和 DOM 节点错误复用。只有列表永远不增删、不排序且没有内部状态时,才可以考虑使用下标。

虚拟列表

当列表包含成千上万条数据时,即使 Diff 很快,创建和维护大量真实 DOM 仍然昂贵。虚拟列表只渲染可视区域附近的项目,适合高度固定或可以估算高度的长列表。

使用虚拟列表前应处理好:

  • 行高变化和动态高度测量;
  • 键盘导航、滚动定位和加载更多;
  • 快速滚动时的占位高度与白屏问题。

v-memov-once

v-memo 适合依赖明确、更新频率低的大型子树;依赖没有变化时可以跳过子树更新:

<div v-for="item in items" :key="item.id" v-memo="[item.id, item.done]">
  {{ item.name }}
</div>

v-once 适合真正只需要渲染一次的内容。两者都应在确认存在更新瓶颈后使用,否则会增加理解和维护成本。Patch Flag、静态提升和 Block Tree 等编译时优化见Diff 算法

v-ifv-show

  • v-if:条件为假时不创建 DOM,适合不常显示或创建成本较高的内容;
  • v-show:始终创建 DOM,只切换 display,适合频繁切换且内容较轻的区域。

选择时要同时考虑初始渲染成本、切换频率和隐藏内容是否占用资源,不能简单归纳为“频繁切换永远使用 v-show”。

五、事件、网络和缓存

高频滚动、拖拽和输入事件应避免在每次触发时执行昂贵逻辑,必要时使用节流、合并更新或把计算移出事件回调。事件监听、定时器和订阅必须在组件卸载时清理:

const timer = setInterval(refresh, 5000)

onUnmounted(() => {
  clearInterval(timer)
})

请求也要关注竞态:组件已经卸载或查询条件已经变化时,旧请求的结果不应覆盖新结果。可以使用 AbortController 或请求序列号取消、忽略过期响应。

KeepAlive 能缓存组件实例,减少反复创建的成本,但也会保留状态和资源。应配合 includeexcludemax 控制范围,并在 deactivated 时暂停不必要的轮询和监听。详见Keep-Alive

六、常见误区

  • 不是所有计算都需要 computed,不是所有列表都需要虚拟化;
  • shallowRefmarkRaw 会改变响应式语义,不能只为“更快”而全局使用;
  • nextTick 只保证 DOM 更新批次已经刷新,不会让昂贵计算自动变快;
  • v-memov-once 可能让界面显示旧数据,使用前要确认依赖完整;
  • 防抖、节流、懒加载和缓存都会引入延迟、状态或一致性成本。

总结

Vue 性能优化可以按下面的顺序思考:

测量瓶颈
减少首屏加载和网络成本
缩小响应式与组件更新范围
减少列表和真实 DOM 数量
清理副作用并复测

一句话总结:先让不需要加载的内容不加载,让不需要更新的组件不更新,让不需要存在的 DOM 不存在,最后用数据验证优化是否有效。