响应式数据劫持有显著性能开销,主要体现为proxy实例爆炸、依赖追踪链过长和更新扩散失控;shallowref/shallowreactive通过限制劫持深度实现精准减负,配合虚拟滚动、分页、toraw等才能真正支撑万级数据。

响应式数据劫持本身就有性能开销,不是“有没有影响”的问题,而是“影响有多大、在哪体现、怎么控”。Vue 的 reactive 和 ref 默认会对整个对象或数组递归代理,遇到万级数据时,这个开销会立刻变成瓶颈——不是卡在渲染,而是卡在初始化那一秒。
劫持开销到底花在哪?
核心是三件事同时发生:
-
Proxy 实例爆炸:10,000 条记录的数组,用
ref会为每一条对象新建一个Proxy,产生上万个代理实例,内存占用翻倍,GC 压力陡增 -
依赖追踪链过长:每个属性读取都会触发
track(),模板里哪怕只用item.id,Vue 仍为item.name、item.desc等所有字段建立依赖,白建一堆没用的链接 -
更新扩散失控:改一个深层字段(如
list[0].children[2].title),若整个结构都是 reactive,可能触发数百个 effect 重运行,哪怕页面根本没显示那个字段
shallowRef 和 shallowReactive 是什么解药?
它们不改变响应式本质,只是把“劫持范围”从“全树穿透”收窄到“表层绑定”,属于精准减负:
-
shallowRef:只让.value可被监听,内部仍是普通 JS 对象。适合分页加载、搜索全量替换等「整体换新」场景 -
shallowReactive:只代理对象第一层属性,state.list或state.total改了会更新,但state.list[0].name = 'x'不会触发视图刷新
实测:5000 条扁平列表初始化,shallowReactive 比 reactive 快 40%~60%,内存分配减少约 1/3。
v-for 渲染不能只靠 shallow,还得配对操作
shallow 本身不解决 DOM 复用和滚动卡顿,必须配合正确写法才能生效:
- 模板中别直接写
v-for="item in list.value",推荐用computed中转:const items = computed(() => list.value),再写v-for="item in items" - key 一定得有,且必须稳定(优先用
item.id,不用index),shallow 不改变 Vue 的 diff 逻辑 - 需要局部响应(比如勾选状态),不要污染原始数据:用独立的
ref(new Set())存 ID 集合,渲染时靠selectedIds.has(item.id)判断
真正扛住万级数据的组合拳
shallow 是响应式层的优化,但大数据渲染是系统工程,单靠它不够:
- 虚拟滚动(必选):只渲染可视区域 20~30 条,其余用占位 div 撑高,DOM 节点数从 10000→30,滚动帧率从掉帧变 60fps
- 分页 or 懒加载:首屏只拉第一页,滚动到底部再 fetch 下一页,服务端配合 offset/limit 或 cursor 分页
- toRaw + markRaw 隔离不可变数据:日志数组、导出 JSON、图表原始配置等,明确告诉 Vue “别代理”,避免无谓 Proxy
- Web Worker 做预处理:排序、过滤、聚合等计算移到 Worker,主线程专注渲染,不卡 UI
不复杂但容易忽略:性能提升从来不是某个 API 的魔法,而是清楚知道数据谁该响应、谁不该动、谁只读、谁要算——然后一层一层拆解控制。











