专栏首页code秘密花园如何实现一个高性能可渲染大数据的Tree组件

如何实现一个高性能可渲染大数据的Tree组件

作者:jayzou

https://segmentfault.com/a/1190000021228976

背景

项目中需要渲染一个5000+节点的树组件,但是在引入element Tree组件之后发现性能非常差,无论是滚动、展开/收起节点还是点击节点卡顿都非常明显,利用performance跑一下性能数据发现到问题所在

从上图可以看到,除去Idle之外,总共花费的时间为12s,其中Scripting花了10s

从上图可以看出,Scripting期间除了 Observe 之外,大部分时间都在调用createChildren来创建vue实例

优化思路

从上面的分析可以看出引发的性能问题都是因为渲染的节点过多导致,那么要解决这个问题就是尽量减少节点的渲染,然而在业界中与之相类似的解决方案就是虚拟列表 虚拟列表的核心概念就是 根据滚动来控制可视区域渲染的列表 这样一来,就能大幅度减少节点的渲染,提升性能

具体的步骤如下:

  1. 将递归结构的tree数据“拍平”,但是保留parent以及child的引用(一方面是为了方便查找子级和父级节点的引用,另一方面是为了方便计算可视区域的list数据)
  2. 动态计算滚动区域的高度(很多虚拟长列表的组件都是固定高度的,但是因为这里是tree,需要折叠/展开节点,所以是动态计算高度)
  3. 根据可见的高度以及滚动的距离渲染相应的节点

代码实现

最简代码实现

<template>
  <div class="b-tree" @scroll="handleScroll">
    <div class="b-tree__phantom" :style="{ height: contentHeight }"></div>
    <div
      class="b-tree__content"
      :style="{ transform: `translateY(${offset}px)` }"
    >
      <div
        v-for="(item, index) in visibleData"
        :key="item.id"
        class="b-tree__list-view"
        :style="{
          paddingLeft: 18 * (item.level - 1) + 'px'
        }"
      >
      <i :class="item.expand ? 'b-tree__expand' : 'b-tree__close' " v-if="item.children && item.children.length" />
        <slot :item="item" :index="index"></slot>
      </div>
    </div>
  </div>
</template>

<style>
.b-tree {
  position: relative;
  height: 500px;
  overflow-y: scroll;
}
.b-tree__phantom {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1;
}
.b-tree__content {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  min-height: 100px;
}
.b-tree__list-view{
  display: flex;
  align-items: center;
  cursor: pointer;
}
.b-tree__content__item {
  padding: 5px;
  box-sizing: border-box;

  display: flex;
  justify-content: space-between;
  position: relative;
  align-items: center;
  cursor: pointer;
}
.b-tree__content__item:hover,
.b-tree__content__item__selected {
  background-color: #d7d7d7;
}
.b-tree__content__item__icon {
  position: absolute;
  left: 0;
  color: #c0c4cc;
  z-index: 10;
}
.b-tree__close{
    display:inline-block;
    width:0;
    height:0;
    overflow:hidden;
    font-size:0;
  margin-right: 5px;
    border-width:5px;
    border-color:transparent transparent transparent #C0C4CC;
    border-style:dashed dashed dashed solid
}
.b-tree__expand{
    display:inline-block;
    width:0;
    height:0;
    overflow:hidden;
    font-size:0;
  margin-right: 5px;
    border-width:5px;
    border-color:#C0C4CC transparent transparent transparent;
    border-style:solid dashed dashed dashed
}
</style>

<script>
export default {
  name: "bigTree",
  props: {
    tree: {
      type: Array,
      required: true,
      default: []
    },
    defaultExpand: {
      type: Boolean,
      required: false,
      default: false
    },
    option: {
      // 配置对象
      type: Object,
      required: true,
      default: {}
    }
  },
  data() {
    return {
      offset: 0, // translateY偏移量
      visibleData: []
    };
  },
  computed: {
    contentHeight() {
      return (
        (this.flattenTree || []).filter(item => item.visible).length *
          this.option.itemHeight +
        "px"
      );
    },
    flattenTree() {
      const flatten = function(
        list,
        childKey = "children",
        level = 1,
        parent = null,
        defaultExpand = true
      ) {
        let arr = [];
        list.forEach(item => {
          item.level = level;
          if (item.expand === undefined) {
            item.expand = defaultExpand;
          }
          if (item.visible === undefined) {
            item.visible = true;
          }
          if (!parent.visible || !parent.expand) {
            item.visible = false;
          }
          item.parent = parent;
          arr.push(item);
          if (item[childKey]) {
            arr.push(
              ...flatten(
                item[childKey],
                childKey,
                level + 1,
                item,
                defaultExpand
              )
            );
          }
        });
        return arr;
      };
      return flatten(this.tree, "children", 1, {
        level: 0,
        visible: true,
        expand: true,
        children: this.tree
      });
    }
  },
  mounted() {
    this.updateVisibleData();
  },
  methods: {
    handleScroll(e) {
      const scrollTop = e.target.scrollTop
      this.updateVisibleData(scrollTop)
    },

    updateVisibleData(scrollTop = 0) {
      const start = Math.floor(scrollTop / this.option.itemHeight);
      const end = start + this.option.visibleCount;
      const allVisibleData = (this.flattenTree || []).filter(
        item => item.visible
      );
      this.visibleData = allVisibleData.slice(start, end);
      this.offset = start * this.option.itemHeight;
    }
  }
};
</script>

细节如下:

  1. 整个容器使用相对定位是为了避免在滚动中引起页面回流
  2. phantom 容器为了撑开高度,让滚动条出现
  3. flattenTree 为了拍平 递归结构的tree数据,同时添加level、expand、visibel属性,分别代表节点层级、是否展开、是否可视
  4. contentHeight 动态计算容器的高度,隐藏(收起)节点不应该计算在总高度里面

这样一来渲染大数据的tree组件就有了基本的雏形,接下来看看节点展开/收起如何实现

节点展开收起

在flattenTree中保留了针对子级的引用,展开/收起的话,只需要对子级进行显示/隐藏即可

{
    methods: {
         //展开节点
        expand(item) {
          item.expand = true;
          this.recursionVisible(item.children, true);
        },
        //折叠节点
        collapse(item) {
          item.expand = false;
          this.recursionVisible(item.children, false);
        },
        //递归节点
        recursionVisible(children, status) {
          children.forEach(node => {
            node.visible = status;
            if (node.children) {
              this.recursionVisible(node.children, status);
            }
          })
        }
}

结论

对比下优化前和优化后的一些性能数据

element tree组件

初次渲染(全收起)

scripting: 11525ms rendering: 2041ms 注:全展开直接卡死

scripting: 84ms rendering: 683ms

优化后的tree组件

首次渲染(全展开)

scripting: 1671ms 对比优化前性能提升 6.8倍 rendering: 31ms 对比优化前性能提升 65倍

节点展开

scripting: 86ms 优化前性能一致 rendering: 6ms 对比优化前性能提升 113倍

big-tree组件

最终封装成 vue-big-tree 组件供调用,欢迎star~~~

本文分享自微信公众号 - code秘密花园(code_mmhy),作者:jayzou

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-12-13

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 跨平台解决方案的技术分析

    近 20 年是中国互联网蓬勃发展的时代,以 2010 年为界限,前 10 年是 PC 互联网时代,PC 互联网时代培养了国民上网冲浪的用户习惯,为后 10 多年...

    winty
  • 跨平台解决方案的技术分析

    近 20 年是中国互联网蓬勃发展的时代,以 2010 年为界限,前 10 年是 PC 互联网时代,PC 互联网时代培养了国民上网冲浪的用户习惯,为后 10 多年...

    小阿新
  • React Fiber 的作用和原理

    完整高频题库仓库地址:https://github.com/hzfe/awesome-interview

    HZFEStudio
  • 万字长文!总结Vue性能优化方式及原理

    我们在使用 Vue 或其他框架的日常开发中,或多或少的都会遇到一些性能问题,尽管 Vue 内部已经帮助我们做了许多优化,但是还是有些问题是需要我们主动去避免的。...

    @超人
  • VUE 3.0 搞起来!

    javascript艺术
  • 【优化】1141- 网页渲染性能优化 —— 渲染原理

    在讨论性能优化之前,我们有必要了解一些浏览器的渲染原理。不同的浏览器进行渲染有着不同的实现方式,但是大体流程都是差不多的,我们通过 Chrome 浏览器来大致了...

    pingan8787
  • 深入探讨前端UI框架

    深入探讨前端UI框架 1 前言 先说说这篇文章的由来 最近看riot的源码,发现它很像angular的dirty check,每个component ( tag...

    IMWeb前端团队
  • 深入探讨前端UI框架

    最近看riot的源码,发现它很像angular的dirty check,每个component ( tag )都保存一个expressions数组,更新时,遍历...

    IMWeb前端团队
  • 面试官问我Chrome浏览器的渲染原理(6000字长文)

    对于HTML,css和JavaScript是如何变成页面的,这个问题你了解过吗?浏览器究竟在背后都做了些什么事情呢?让我们去了解浏览器的渲染原理,是通往更深层次...

    达达前端
  • 【云+社区年度征文】面试官问我Chrome浏览器的渲染原理(6000字长文)

    对于HTML,css和JavaScript是如何变成页面的,这个问题你了解过吗?浏览器究竟在背后都做了些什么事情呢?让我们去了解浏览器的渲染原理,是通往更深层次...

    达达前端
  • 【专业技术】chromium GPU 硬件加速合成

    前言: 在传统浏览器网页渲染实现方案中,网页完全依赖CPU的能力去渲染网页(软件渲染简介:网页生成一张bitmap丢给CPU去绘制),然而一台机器的CPU不仅...

    程序员互动联盟
  • 浏览器加载解析渲染机制的全面解析

    (注1:如果有问题欢迎留言探讨,一起学习!转载请注明出处,喜欢可以点个赞哦!) (注2:更多内容请查看我的目录。)

    love丁酥酥
  • 前端tree组件,10000个树节点,从14.65s到0.49s

    递归组件版 tree 点击节点性能分析图:点击节点处理速度: 10.19s - 0.357s = 9.833s ≈ 9.83s

    前端劝退师
  • react fiber 到底有多细

    Fiber 是对 React 核心算法的重构,facebook 团队使用两年多的时间去重构 React 的核心算法,在 React16 以上的版本中引入了 Fi...

    有赞coder
  • Web 性能优化-页面重绘和回流(重排)

    常见的优化网络请求的方法有:DNS Lookup,减少重定向,避免 JS、CSS 阻塞,并行请求,代码压缩,缓存,按需加载,前端模块化…

    李振
  • 干货 | React Fiber 初探

    携程技术
  • 详解微信原生小程序架构及同构方案

    最近实习中参与了H5项目向小程序迁移的工作,在微信官方文档和一些帖子上学习了小程序运行机制和底层原理,以及与Web页面的区别,在此基础上又看了一些关于小程序同构...

    极乐君
  • 2020年,需要了解 Vue3 的哪些知识

    Vue 是目前前沿开发中最热门的框架之一,到2019年每周的下载率翻了一番。2020 年初 Vue3的发布还会增加它的受欢迎程度。

    前端小智@大迁世界
  • 深入浅出浏览器渲染原理

    浏览器的内核是指支持浏览器运行的最核心的程序,分为两个部分的,一是渲染引擎,另一个是JS引擎。渲染引擎在不同的浏览器中也不是都相同的。比如在 Firefox 中...

    Fundebug

扫码关注云+社区

领取腾讯云代金券