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

第一步:确认卡在哪个阶段
- 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 问题。