background

The project needed to render a Tree component of 5000+ nodes, but after the introduction of element Tree component, the performance was found to be very poor, whether scrolling, expanding/unwinding nodes or clicking nodes were very obvious, and the problem was found by running performance data

12s
10s

createChildren

Optimization idea

Can be seen from the above analysis of performance problems because rendering nodes lead to too much, so to solve this problem is to minimize the render of the node, however, in the industry and the similar solution is the core concept of the virtual virtual list according to scroll to control rendering a list of the visible area In this way, You can dramatically reduce node rendering and improve performance

The specific steps are as follows:

  1. Flatten the recursive tree data, but keep references to parent and child (on the one hand, to find references to child and parent nodes, and on the other hand, to calculate the list of visible areas)
  2. Dynamically calculate the height of the scroll area (many components of the virtual long list are fixed height, but because this is a tree, the node needs to be collapsed/expanded, so the height is dynamically calculated)
  3. Render the corresponding nodes according to the height visible and how far they are rolled

Code implementation

Minimal code implementation

<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: {
      // Configure objects
      type: Object.required: true.default: {}
    }
  },
  data() {
    return {
      offset: 0.// translateY offset
      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>

Copy the code

Here are the details:

  1. The entire container uses relative positioning to avoid page backflow during scrolling
  2. The phantom container makes the scroll bar appear in order to spread the height
  3. FlattenTree adds level, expand, and Visibel attributes to flat tree data of recursive structure, representing node level, expansion, and visibility respectively
  4. ContentHeight dynamically calculates the height of containers; hidden (stowed) nodes should not be counted as part of the total height

This gives you a basic prototype of the tree component for rendering big data. How does node expansion/collapse work

Node expansion and collapse

References to children are kept in flattenTree, and only need to be shown/hidden by expanding/collapsing them

{
	methods: {
		 // Expand the node
		expand(item) {
		  item.expand = true;
		  this.recursionVisible(item.children, true);
		},
		// Collapse the node
		collapse(item) {
		  item.expand = false;
		  this.recursionVisible(item.children, false);
		},
		// recursive node
		recursionVisible(children, status) {
		  children.forEach(node= > {
			node.visible = status;
			if (node.children) {
			  this.recursionVisible(node.children, status); }}}})Copy the code

conclusion

Compare some performance data before and after optimization

Element tree component

First render (all folded up)

scripting:
rendering:

scripting:
rendering:

Optimized tree component

First render (full unfold)

scripting:
6.8 times
rendering:
65 times

Node expansion

scripting:
rendering:
113 times

The big tree components

Finally encapsulated into vuE-big-tree component for call, welcome star~~~