性能问题不会凭空出现,它们总是落在具体的执行环节里。搞清楚“卡”在哪儿,你才能用对工具、用准方法。根据 Vue 应用的运行特征,瓶颈通常可以归为三类。
渲染瓶颈:界面更新太慢或太频繁
症状:页面滚动卡顿、动画掉帧、输入框输入延迟、列表展开收起时明显顿挫。在 Vue DevTools 的 Performance 面板中,能看到某些组件的重渲染次数异常高,或者单次渲染耗时过长。
常见原因:
- 不必要的组件重渲染:父组件更新导致子组件跟着无意义地执行,即便子组件的 props 和依赖没有变化。
- 过大的组件树或动态节点数量:一个长列表全量渲染几千个节点,或者一个页面包含了太多响应式绑定的 DOM 结构。
- 高频触发的状态变更:比如
mousemove事件中频繁修改响应式数据,导致视图不断重绘。
实用应对策略:
- 限制重渲染范围:合理拆分组件,让状态尽可能地“下沉”到最小的作用域内;对纯静态区域使用
v-once,对部分稳定的动态片段使用v-memo。 - 缓存不常变的子树:用
<KeepAlive>包裹频繁切换的视图(如 Tab 页),避免反复销毁与重建。 - 处理长列表:核心是“只渲染看得见的部分”。推荐使用
vue-virtual-scroller等虚拟列表方案,或者自行实现可视区内的懒加载。 - 防抖节流高频事件:
scroll、resize、input等事件的处理函数中,对实际触发视图更新的操作加一道防抖或节流(可封装为自定义 hook)。
计算瓶颈:JavaScript 主线程被吃满
症状:点击一个按钮后整个页面卡死几百毫秒;输入筛选条件时输入框卡住不动;在 Performance 录制的火焰图中,一段黄色的 JavaScript 长任务霸占了主线程。
常见原因:
- 在计算属性或侦听器中执行了大开销的同步运算:比如对一个百万长度的数组进行排序、过滤和遍历,每一次依赖变化都重新执行一次。
- 模板中写了复杂的表达式:在
{{ }}内直接调用一个每次都会重算纯函数,或者数据量很大的格式化方法。 - 不合理的 computed 依赖:计算属性依赖了另一个计算属性,层层叠加,且在模板中大量引用。
实用应对策略:
- 用 computed 替代 method 调用:确保结果被缓存,只在相关依赖变化时才重新计算,避免在模板中每次渲染都执行重复逻辑。
- 避免长任务阻塞:将重型计算移到异步执行,比如使用
requestIdleCallback分片处理,或者借助Web Worker在独立线程里完成计算,完成后通过消息更新到主线程的响应式数据。 - 数据量控制:对于不会一次性用于渲染的大数据集,尽量在后端做分页或筛选,前端只保留当前界面真正需要展示的少量数据。
- 善用
shallowRef/shallowReactive:对于结构庞大但只有顶层引用会变化的数据,避免深度代理带来的额外开销。
网络瓶颈:资源加载慢,用户等待久
症状:白屏时间长、首屏加载缓慢、切换子页面时出现加载中的延迟。在浏览器 Network 面板中,能看到接口响应慢、JS/CSS/图片等静态资源体积大或下载延迟高。Lighthouse 报告中 SEO/Performance 分数偏低。
常见原因:
- 未做代码分割:整个应用打成一个巨包,浏览器需要下载完所有 JS 才能开始渲染。
- 接口数据冗余或串行请求:一个页面初始化需要调用三四个相互依赖的接口,总等待时间叠加。
- 未经优化的静态资源:大图片直接使用原图、非系统字体的 ttf 文件过大、第三方库全量引入未按需加载。
实用应对策略:
- 路由级与组件级代码分割:配置 Vue Router 的
() => import()实现按路由懒加载;大体积的组件(如编辑器、图表)可以用defineAsyncComponent做异步加载。 - 数据预取与缓存:使用
TanStack Query (Vue Query)等方案管理服务端状态,它的缓存、自动重取、预加载能力可以有效减少不必要的网络请求,并掩盖加载延迟。 - 优化资源本身:图片转 WebP 格式并做响应式尺寸处理,开启 Gzip/Brotli 压缩,通过 CDN 分发静态资源和常用库。
- 首屏关键资源内联或预加载:对首屏的关键 CSS 内联,对关键 JS 使用
<link rel="preload">,减少请求链长度。 - 使用骨架屏与过渡态:虽然这不减少网络耗时,但能显著改善用户的感知速度,避免白屏焦虑。
三者常常联动:一个网络请求慢会导致数据到位晚,进而可能让后续的渲染和计算集中爆发。排查时,先用工具定位主要耗时在哪个阶段(网络→计算→渲染),然后按上面的分类对症下药,避免漫无目的地修改代码。