「Vue 3 比 Vue 2 快」是框架层面的结论,业务代码里的卡顿通常另有原因。排查顺序应该是:先确认是渲染、计算还是网络,再动手改。

Vue 3 渲染卡顿排查:key 的使用、大列表与响应式开销 示意图

第一步:确认卡在哪个阶段

  • Performance 面板录制一次交互,看主线程是长任务(JS 执行)还是大面积 Recalculate Style;
  • Vue DevTools 的 Performance 页看组件渲染耗时,找出最贵的那一个;
  • 卡顿与滚动强相关时,问题多在列表项数量与图片解码,而不是响应式系统。

key 用 index,等于让 diff 失效

<li v-for="(item, index) in list" :key="index">{{ item.name }}</li>

在列表头部插入或删除元素时,index 会整体错位,Vue 只能复用「错误的节点」再逐个更新内容,输入框光标、复选框状态都会串到相邻行。换成业务唯一 id,diff 才能精确复用:

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

不要把大对象变成响应式

响应式的成本与对象规模成正比:每个属性都要递归代理,读取时还要收集依赖。接口返回的几千行表格数据、地图坐标、图表配置,如果只需要整体替换,用 shallowRef:

import { shallowRef, triggerRef } from 'vue';

const rows = shallowRef([]);

rows.value = await fetchRows();

rows.value.push(row);
triggerRef(rows);

同理,只关心第一层字段的对象用 shallowReactive。原则是:只有会驱动视图变化的字段才值得响应式。

计算属性别做重活

computed 有缓存,但依赖一变就整体重算。把「过滤 + 排序 + 统计」塞进一个 computed,用户每输入一个字都会全量跑一遍:

const filtered = computed(() => rows.value.filter(match));
const sorted = computed(() => [...filtered.value].sort(byDate));
const total = computed(() => sorted.value.reduce(sum, 0));

拆成多个小的 computed 按需组合,或者把重活移出渲染路径(防抖后再算、交给 Web Worker)。另外不要在模板里调用方法做计算:模板每次重渲染都会重新执行,而 computed 只在依赖变化时执行。

v-if 与 v-show:按切换频率选

  • 切换频繁、结构简单:用 v-show,只改 display;
  • 初始不需要渲染、切换不频繁:用 v-if,省掉创建与销毁的成本;
  • 列表里同时用 v-for 与 v-if,先把数据过滤好再循环,而不是在模板里逐项判断。

长列表:分页或虚拟滚动

一次渲染两千行 DOM,无论框架多快都会卡。优先分页或「加载更多」;确实需要无限滚动时用虚拟列表,只渲染可视区域内的若干行。再配合图片 loading="lazy" 与固定高度占位,滚动会稳定很多。

小结

先用工具定位,再按「key 是否正确 → 数据是否过度响应式 → 计算是否重复 → DOM 是否过多」的顺序逐项排查,通常改一两处就能看到明显差别。数据侧的列表优化(避免一条条查库)见Eloquent 的 N+1 问题。