背景
在 Vue 项目中,渲染一个包含 10000+ 节点 的树形结构(树形选择器)时,如果直接将接口返回的数据交给响应式系统,性能会明显下降。
当数据层级很深、字段很多时,Vue 会对整棵树进行深度代理,这一步的开销非常大。即便节点是点击后才展开并渲染,在响应式代理阶段依然会消耗大量时间,导致页面卡顿、交互延迟。
本文将通过 数据清洗 与 markRaw 跳过响应式代理 两种方式,把渲染耗时从 184ms 降到 17ms,性能提升超过 10 倍。
树形选择器
原始数据,不做处理,直接赋值到ref
数据格式示例如下:
{
id:1,
name:'根节点',
level:1,
children:[
id:12,
name:'子节点',
level:2,
children:[]
]
... // 其他字段
}
请求接口获取节点
const treeData = ref({})
// 获取树数据
const getTreeData = async () => {
try {
const res = await getData()
const now = Date.now()
console.log('更新前', now)
treeData.value = res
treeDataReset = cloneDeep(treeData.value)
nextTick(() => {
console.log('更新后 耗时 ms', Date.now() - now)
})
} catch (err) {
console.warn(err)
}
}
可以看到,Vue 会对整个树对象进行代理,导致页面渲染耗时高达 184ms。
优化点 1:数据清洗,去除无用字段
当数据中存在大量无关字段时,会显著影响性能。
// 格式化树形数据
const formatTreeData = ({ id, name, level, parentName, children }: any): TreeNode => {
// 提取需要的字段
const formattedNode: TreeNode = {
name,
id,
level,
parentName
}
// 递归处理子节点
if (children && children.length > 0) {
formattedNode.children = children.map((child: any) => formatTreeData(child))
}
return formattedNode
}
treeData.value = formatTreeData(res)
结果:去除多余字段后,渲染耗时降至 82ms
优化点 2:使用 markRaw
markRaw 的作用是为对象添加 __v_skip 标记,使 Vue 响应式系统跳过该对象的深度观测,数据变化将不会触发视图更新,从而显著提升性能。
treeData.value = markRaw(formatTreeData(res))
结果:使用 markRaw 后,让 Vue 完全跳过数据代理,渲染耗时直接降到 17ms。
markRaw怎么停止响应式监测?
markRaw 只是给对象加上 __v_skip = true 标记。
依赖收集时,traverse 发现该标记会直接跳过,避免递归和响应式处理。
traverse 在 Watcher.get 中执行,而 Watcher 由 effect 创建。
graph TD
A[markRaw 设置 __v_skip=true] --> B[traverse 检测到标记跳过对象]
B --> C[Watcher.get 调用 traverse]
C --> D[effect 创建 Watcher]
观察下面的vue源码
性能对比表
| 优化方式 | 渲染耗时 |
|---|---|
| 原始数据(包含所有字段,Vue 深度代理) | 184ms |
| 数据清洗(去除无关字段) | 82ms |
数据清洗 + markRaw(跳过响应式代理) | 17ms |
总结
- 数据清洗——只保留必要字段,减少代理对象体积。
markRaw——直接跳过响应式,让 Vue 不再对数据做深度追踪。
适合静态且只读的大型数据结构。