一次100W+数据级别的渲染优化
组织架构的列表页有关于公司人员架构的树形结构展示,某大客户有10万员工,造成组织架构的列表渲染卡顿,用户点击经常造成页面崩溃。
需求背景:左边是树形目录,多层级展示,层级结构未作限制。点击左边目录会展示对应的列表,点击右边对应用户的组织属性,也会联动左边的目录展示。
技术背景:vue2 + element el-tree
问题:1w数据页面展示无影响,当数据量达到5w+时页面开始卡顿甚至崩溃(导致公司某大客户持续投诉了一个月,他们有十几万员工)。

从技术角度来看,主要由以下原因造成:
1.虚拟dom的渲染开销
Vue 使用虚拟 DOM 来高效地更新真实 DOM。然而,虚拟 DOM 的创建和更新仍然需要一定的计算开销:
- 节点数量过多:1 万条数据意味着需要创建 1 万个虚拟 DOM 节点,这会占用大量内存和计算资源。
- 渲染时间过长:大量节点的渲染会导致主线程阻塞,页面出现卡顿。
2.响应式数据监听的开销
Vue 会对组件的 data 进行响应式处理,即递归地将数据属性转换为 getter/setter
3.组件生命周期钩子的执行
每个 el-tree 节点都是一个组件,Vue 会为每个组件执行生命周期钩子(如 created、mounted)
4.el-tree 的实现机制
el-tree会一次性渲染所有节点,并且为每个节点绑定事件(如点击、展开/折叠等)
综合以上原因,结合需求背景,分析得出以下几点:
- 页面左右两块区域是互相联动,所有必须数据节点必须全部渲染出来
- 目录层级未作限制。根据客户实际情况,battle产品,最终将最大层级限制到5层
- 接口直接返回嵌套的数据结构,对于深度较大的树,递归调用栈会占用大量内存。改成使用扁平数据结构,修改z-tree适配vue进行目录渲染。
show me the code
step 1:安装依赖, 当前ztree版本3.5.24
npm install ztree jquery
ztree初始化逻辑:
// 1. 引入样式和核心文件
import 'ztree/css/zTreeStyle/zTreeStyle.css'
import 'ztree'
// 2. 定义容器
<div id="treeId" class="ztree"></div>
// 3. 初始化实例(需在 DOM 加载完成后执行)
const setting = {}; // 配置项
const zNodes = []; // 数据
const zTreeObj = $.fn.zTree.init($('#treeId'), setting, zNodes);
step 2: 封装ztree组件,创建ZTree.vue文件
<template>
<div :id="treeId" class="ztree"></div>
</template>
<script>
import 'ztree/css/zTreeStyle/zTreeStyle.css'
import 'ztree'
export default {
name: 'ZTree',
props: {
data: Array, // 原始数据
setting: Object // zTree 配置
},
data() {
return {
treeId: `ztree_${Math.random().toString(36).substr(2, 9)}`, // 唯一 ID
zTreeObj: null // zTree 实例
}
},
mounted() {
this.initZTree(); // 初始化
},
methods: {
initZTree() {
this.zTreeObj = $.fn.zTree.init(
$(`#${this.treeId}`),
this.setting,
this.data
);
}
}
}
</script>
通过 watch 监听数据变化,重新渲染:
watch: {
data(newData) {
if (this.zTreeObj) {
this.zTreeObj.destroy(); // 销毁旧实例
}
this.initZTree(newData); // 重新初始化
}
}
step3: 字段映射
接口返回数据字段如下
[
{ id: 1, label: '根节点', parentId: null },
{ id: 2, label: '子节点', parentId: 1 }
]
zTree 默认需要以下字段
[
{ id: 1, name: '根节点', pId: null },
{ id: 2, name: '子节点', pId: 1 }
]
在组件中新增 fieldMapping 属性,支持字段映射:
props: {
fieldMapping: {
type: Object,
default: () => ({
id: 'id',
name: 'name',
pId: 'pId'
})
}
}
通过计算属性 convertedData 转换数据:
computed: {
convertedData() {
return this.data.map(node => ({
id: node[this.fieldMapping.id],
name: node[this.fieldMapping.name],
pId: node[this.fieldMapping.pId]
}));
}
}
ZTree.vue完整代码如下:
<template>
<div :id="treeId" class="ztree"></div>
</template>
<script>
import "ztree/css/zTreeStyle/zTreeStyle.css";
import "ztree";
export default {
name: "ZTree",
props: {
// 树数据
data: {
type: Array,
default: () => [],
},
// 字段映射配置
fieldMapping: {
type: Object,
default: () => ({
id: "id", // 节点唯一标识字段
name: "name", // 节点显示文本字段
pId: "pId", // 父节点标识字段
level: "level", // 层级字段(可选)
}),
},
// zTree 配置
setting: {
type: Object,
default: () => ({}),
},
},
data() {
return {
treeId: `ztree_${Math.random().toString(36).substr(2, 9)}`, // 生成唯一 ID
zTreeObj: null, // zTree 实例
};
},
computed: {
// 转换后的数据
convertedData() {
const mapping = this.fieldMapping;
const data = this.data;
// 一次遍历完成数据转换
const result = data.map((node) => {
const converted = {};
Object.keys(mapping).forEach((key) => {
const mapValue = mapping[key];
converted[key] =
typeof mapValue === "function"
? mapValue(node) // 动态计算字段值
: node[mapValue] ?? node[key]; // 直接映射字段值
});
return converted;
});
return result;
},
},
watch: {
// 监听数据变化,重新加载树
convertedData: {
handler(newData) {
this.reloadTree(newData);
},
deep: true,
},
},
mounted() {
this.initZTree(); // 初始化 zTree
console.log("zTree 实例:", this.zTreeObj); // 打印 zTree 实例
},
methods: {
// 初始化 zTree
initZTree() {
this.zTreeObj = window.$.fn.zTree.init(
window.$(`#${this.treeId}`),
this.setting,
this.convertedData
);
},
// 重新加载树
reloadTree(data) {
if (this.zTreeObj) {
this.zTreeObj.destroy(); // 销毁旧实例
}
this.initZTree(data); // 初始化新实例
},
},
};
</script>
<style scoped>
.ztree {
width: 100%;
height: 100%;
overflow: auto;
}
</style>
step4: 父组件调用ztree组件
启用扁平结构
setting: {
data: {
simpleData: {
enable: true, // 启用扁平模式
idKey: 'id', // 节点 ID 字段名
pIdKey: 'pId' // 父节点 ID 字段名
}
}
}
父组件代码如下:
<template>
<div id="app">
<ZTree :data="treeData" :fieldMapping="fieldMap" :setting="treeSetting" />
</div>
</template>
<script>
import ZTree from "./components/ZTree.vue";
import { generateFlatTreeData } from "./utils";
export default {
components: {
ZTree,
},
data() {
return {
// 接口返回的扁平结构数据
treeData: [
{ id: 1, label: "根节点", level: 1, parentId: null },
{ id: 2, label: "子节点 1", level: 2, parentId: 1 },
{ id: 3, label: "子节点 2", level: 2, parentId: 1 },
],
// 字段映射规则
fieldMap: {
id: "id",
name: "label", // 将接口的 label 字段映射为 zTree 的 name 字段
pId: "parentId", // 将接口的 parentId 字段映射为 zTree 的 pId 字段
level: "level",
},
// zTree 配置
treeSetting: {
data: {
simpleData: {
enable: true, // 启用扁平结构
},
},
view: {
showIcon: true, // 显示图标
},
},
};
},
mounted() {
// 模拟异步请求
const data = generateFlatTreeData(5, 100000);
// const data = generateFlatTreeData(5, 800000);
console.log("data.length", data);
this.treeData = data;
},
};
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
项目完整代码见github: https://github.com/webLion200/vue-ztree

浙公网安备 33010602011771号