一次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会一次性渲染所有节点,并且为每个节点绑定事件(如点击、展开/折叠等)

综合以上原因,结合需求背景,分析得出以下几点:

  1. 页面左右两块区域是互相联动,所有必须数据节点必须全部渲染出来
  2. 目录层级未作限制。根据客户实际情况,battle产品,最终将最大层级限制到5层
  3. 接口直接返回嵌套的数据结构,对于深度较大的树,递归调用栈会占用大量内存。改成使用扁平数据结构,修改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

posted @ 2025-03-19 15:39  webLion200  阅读(82)  评论(0)    收藏  举报