el-table 树形结构卡顿?懒加载方案实操指南 全栈开发亲测有效
在日常全栈开发中,使用 Element UI(或 Element Plus)的 el-table 组件展示树形结构数据是很常见的需求,比如部门层级、分类目录、权限树等场景。但近期在开发一个包含千级以上节点的树形表格时,我遇到了明显的卡顿问题 - 页面初始化缓慢、展开折叠节点时延迟明显,甚至出现浏览器卡死的情况。

经过多轮调试和方案对比,最终采用懒加载方案彻底解决了卡顿问题,本文将详细记录问题排查过程和懒加载方案的完整实现,供有同样需求的开发者参考。
一、el-table 树形结构卡顿的核心原因
在排查卡顿问题时,我通过浏览器开发者工具(Performance 面板)分析发现,卡顿的核心原因集中在以下几点,也是大多数开发者会踩的坑:
- 全量递归渲染:默认情况下,el-table 会一次性渲染所有层级的树形节点,即使是未展开的子节点也会被渲染到 DOM 中,当节点数量超过 300 条、层级超过 5 层时,DOM 节点数量会急剧增加,导致浏览器重绘和重排压力过大。
- 缺少唯一 row-key:未配置 row-key 或使用 index 作为 row-key,会导致 Vue 的 diff 算法无法高效识别节点,展开折叠时需要重新渲染大量节点,进一步加剧卡顿。
- 不必要的功能冗余:开启了 border、stripe、highlight-current-row 等非必要功能,这些功能会增加样式计算和事件绑定的开销,在大数据量场景下会明显影响性能。
- 单元格渲染复杂:部分列的单元格嵌套了 el-select、el-switch 等复杂组件,循环渲染时会产生大量的组件实例,占用过多内存和 CPU 资源。
二、方案选型:为什么最终选择懒加载?
针对卡顿问题,我测试了多种优化方案,包括基础配置优化、数据扁平化、虚拟滚动、懒加载等,各方案的适配场景和效果如下:
- 基础配置优化(加 row-key、关闭冗余功能):能缓解轻微卡顿,但对于千级以上节点的树形结构,效果有限,无法从根本上解决 DOM 节点过多的问题。
- 数据扁平化:将树形数据转为一维数组,通过 level 控制缩进,减少递归渲染开销,但仍需一次性渲染所有节点,大数据量下依然会卡顿。
- 虚拟滚动:需要升级 Element Plus 或引入第三方组件,改造成本较高,且部分场景下会与树形展开折叠功能冲突,兼容性不佳。
- 懒加载:初始只渲染根节点,点击展开节点时才异步加载对应子节点,从根本上减少了初始 DOM 节点数量,改造成本低、兼容性好,且能适配任意层级和大数据量场景,是最贴合实际开发需求的方案。
经过实际测试,采用懒加载方案后,页面初始化时间从原来的 3-5 秒缩短至 500ms 以内,展开折叠节点时无任何延迟,即使节点数量达到万级,也能保持流畅运行。
三、el-table 树形懒加载完整实现
以下是基于 Vue2 + Element UI 的懒加载方案完整实现,包含模板配置、数据结构、后端配合、加载方法等,适配大多数树形表格场景,可直接复制到项目中使用并根据实际需求调整。
1. 模板配置(核心懒加载属性)
el-table 实现懒加载需配置 3 个核心属性:lazy(开启懒加载)、load(子节点加载方法)、row-key(唯一标识,必须配置),同时通过 tree-props 指定树形结构的子节点字段和是否有子节点的标识字段。
<template>
<div class="table-container">
<el-table
ref="treeTable"
row-key="id" <!-- 必须唯一,建议使用数据库主键 ID,不可用 index -->
lazy <!-- 开启树形懒加载 -->
:load="loadNode" <!-- 子节点加载方法 -->
:data="tableData" <!-- 初始根节点数据 -->
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }" <!-- 树形配置 -->
:border="false" <!-- 关闭不必要功能,减少性能开销 -->
:stripe="false"
:highlight-current-row="false"
>
<!-- 树形缩进列(核心,控制节点缩进) -->
<el-table-column
prop="name"
label="节点名称"
align="left"
:cell-style="({ row }) => ({ paddingLeft: `${row.level * 20}px` })" <!-- 按层级缩进 -->
>
<!-- 可添加自定义图标,区分是否有子节点 -->
<template #default="scope">
<i
class="el-icon-folder"
v-if="scope.row.hasChildren"
style="margin-right: 4px;"
></i>
<i
class="el-icon-file"
v-else
style="margin-right: 4px;"
></i>
{{ scope.row.name }}
</template>
</el-table-column>
<!-- 其他业务列(根据实际需求添加,尽量精简) -->
<el-table-column
prop="code"
label="节点编码"
width="120"
></el-table-column>
<el-table-column
prop="createTime"
label="创建时间"
width="180"
></el-table-column>
</el-table>
</div>
</template>2. 数据结构 前端初始数据 + 后端返回格式
懒加载的核心是 初始只加载根节点,子节点按需加载 ,因此前端初始数据只需包含根节点,且根节点需标记 hasChildren 为 true(表示有子节点),children 字段设为 null(避免初始渲染子节点);后端需提供根据父节点 ID 查询子节点的接口,返回格式与根节点一致。
2.1 前端初始数据
export default {
data() {
return {
// 初始只加载根节点,children 为 null,hasChildren 为 true
tableData: [
{
id: 1,
name: "根节点",
code: "ROOT",
createTime: "2024-01-01 00:00:00",
hasChildren: true, // 标记有子节点,会显示展开图标
children: null // 懒加载前为空,避免初始渲染
}
]
};
}
};2.2 后端接口返回格式(以 JSON 为例)
后端需提供接口(如:/api/tree/children),接收父节点 ID 参数,返回该父节点的所有子节点列表,子节点中需包含 hasChildren 字段(标记是否有下一级子节点):
{
"code": 200,
"message": "success",
"data": {
"list": [
{
"id": 2,
"name": "一级节点 1",
"code": "LEVEL1_01",
"createTime": "2024-01-02 00:00:00",
"hasChildren": true, // 有子节点,可继续展开
"children": null
},
{
"id": 3,
"name": "一级节点 2",
"code": "LEVEL1_02",
"createTime": "2024-01-02 00:00:00",
"hasChildren": false, // 无子节点,不显示展开图标
"children": null
}
]
}
}3. 子节点加载方法(核心逻辑)
loadNode 方法是懒加载的核心,el-table 会在点击展开节点时自动调用该方法,传入当前节点(node)和回调函数(resolve),我们需要在该方法中异步请求子节点数据,然后通过 resolve 返回给 el-table,同时标记节点已加载(避免重复请求)。
export default {
methods: {
/**
* 懒加载子节点
* @param {Object} node - 当前点击的节点对象
* @param {Function} resolve - 回调函数,用于返回子节点数据
*/
async loadNode(node, resolve) {
// 根节点(level=0),直接返回初始根节点数据(避免重复请求)
if (node.level === 0) {
return resolve(this.tableData);
}
// 非根节点,根据父节点 ID 请求子节点
try {
// 调用后端接口,传入父节点 ID(node.row.id 为当前节点的 ID)
const res = await this.$axios.get("/api/tree/children", {
params: { parentId: node.row.id }
});
const childrenList = res.data.data.list;
// 标记当前节点已加载,避免重复点击展开时重复请求
node.row._loaded = true;
// 将子节点赋值给当前节点的 children 字段(可选,便于后续操作)
node.row.children = childrenList;
// 通过 resolve 返回子节点数据,el-table 自动渲染
resolve(childrenList);
} catch (error) {
console.error("加载子节点失败:", error);
resolve([]); // 失败时返回空数组,避免页面报错
}
}
}
};四、懒加载方案的关键注意事项
在实际开发中,除了上述核心实现,还需要注意以下几点,避免出现加载异常或卡顿残留问题:
- row-key 必须唯一:必须使用数据库主键 ID 等唯一标识作为 row-key,不可使用 index,否则会导致节点展开折叠异常、diff 算法失效,甚至出现重复渲染。
- hasChildren 字段必须准确:后端返回的子节点中,hasChildren 需准确标记是否有下一级子节点,若标记错误,会导致无子女节点显示展开图标,或有子节点不显示展开图标。
- 避免重复请求:通过 node.row._loaded 标记节点是否已加载,避免用户多次点击展开图标时重复请求后端接口,减少接口压力和冗余请求。
- 精简列和单元格:尽量减少表格列的数量,避免在单元格中嵌套复杂组件,若必须使用,可采用按需渲染(如 hover 时显示)的方式,降低渲染开销。
- 异常处理:加载子节点失败时,需通过 resolve 返回空数组,避免页面报错或卡死,同时可添加错误提示,提升用户体验。
五、实际效果对比
在未使用懒加载时,千级节点的树形表格初始化需要 3-5 秒,展开折叠节点时延迟 1-2 秒,浏览器控制台会出现 长任务 警告;使用懒加载方案后,初始化时间缩短至 500ms 以内,展开任意节点均无延迟,即使节点数量达到万级,页面依然流畅,彻底解决了卡顿问题。
总结
el-table 树形结构卡顿的核心痛点是 全量渲染导致 DOM 节点过多 ,而懒加载方案通过 按需加载子节点 的方式,从根本上解决了这一问题,具有改造成本低、兼容性好、适配性强的特点,是大数据量树形表格的最优解决方案。本文提供的完整实现代码可直接复制使用,适用于大多数树形结构场景(如部门树、分类树、权限树等),希望能帮助更多开发者避开卡顿坑,提升项目体验。




