Performance optimization of Tree component in massive data: virtual Tree

A few days ago, I added the function of virtual Tree to Santd's Tree component to solve the performance problem of Tree component when rendering massive data.

You may have the opportunity to use the virtual tree in front-end business in the future. Even if you don't use it directly, understanding the virtual tree may also provide you with some new ideas to solve problems.

Santd is the San implementation of Ant Design, serving enterprise level middle and back office products.

0 directory

1 what is a virtual tree?

2 why use virtual trees?

3 how to implement virtual tree?

4 final effect

5 references

1 what is a virtual tree?

Some readers may also know a concept called "virtual list".

Virtual tree and virtual list are essentially the same, but the structure of the original data of the former is tree, while the structure of the original data of the latter is list, and the final form is different.

Whether it is a virtual tree or a virtual list, the core principle is to render only the data in the visual area, that is, the data that users cannot see will not be rendered, which is why it is called "virtual".

In a word, a virtual tree is a tree that renders only the data of the visual area.

2 why use virtual trees?

Because rendering takes time.

If a tree contains massive data, such as tens of thousands of pieces, the rendering of the tree will take a long time.

We can intuitively feel it through a practical example.

The following code uses Santd's common tree component to create a tree with 11110 nodes. We don't need to pay attention to the specific details of the code. We just need to know that it creates a tree with more than 10000 nodes and prints out the rendering time of the tree.

<template>
    <s-tree treeData="{{treeData}}" defaultExpandAll="{{true}}"></s-tree>
</template>

<script>
import {Tree} from 'santd';

function dig(path = '0', level = 3) {
    const list = [];
    for (let i = 0; i < 10; i += 1) {
        const key = `${path}-${i}`;
        const treeNode = {
            title: key,
            key,
        };

        if (level > 0) {
            treeNode.children = dig(key, level - 1);
        }

        list.push(treeNode);
    }
    return list;
}

const start = Date.now();
const treeData = dig();
setTimeout(() => {
    console.log(`Rendering time: ${(Date.now() - start) / 1000} second`);
}, 0);

export default {
    components: {
        's-tree': Tree
    },
    initData() {
        return {
            treeData
        }
    }
}
</script>

Then, we can see that this 11110 node tree has been rendered in Chrome of my 2019 MacBook Pro for 26 seconds, which will last for nearly half a minute.


Such a long time is generally unacceptable, so we need to find a way to solve this problem, which can use the virtual tree.

Virtual tree can greatly reduce the rendering time by rendering only the part of the massive data in the visual area.

3 how to implement virtual tree?

The idea of realizing the virtual tree is to transform the virtual tree into a virtual list, and then decorate the list like a tree when displaying.

According to this idea, the process of realizing virtual tree can be roughly divided into four steps:

  1. Flatten the original data of the tree structure;
  2. Calculate which data is in the visual area;
  3. Simulated rolling;
  4. Decorate the list as a tree.
    Next, let's look at it step by step.

For the time being, you don't need to pay too much attention to the code details. You can first grasp the implementation steps and principles of the virtual tree as a whole, and then look back at the code if necessary.

3.1 flatten the original data of the tree structure

We illustrate this step with simple raw data.

The original data will be a tree structure like the following:

const treeData = [
    {
        key: '0-0',
        children: [
            {
                key: '0-0-0'
            },
            {
                key: '0-0-1'
            }
        ]
    },
    {
        key: '0-1',
        children: [
            {
                key: '0-1-0'
            },
            {
                key: '0-1-1'
            }
        ]
    }
];

This is a simple tree with a depth of 2. There are 2 primary nodes, and each primary node has 2 secondary nodes, so there are 6 nodes in total.

Then we need to flatten it. The key to flattening is to traverse the tree according to depth first. The logic of flattening is as follows:

function flattenTreeData(treeData) {
    const flatNodes = [];
    const dig = treeData =>
        treeData.forEach(treeNode => {
            flatNodes.push(treeNode);

            dig(treeNode.children || []);
        });

     dig(treeData);

    return flatNodes;
}

The data after flattening will be like the following. Here we need to focus on it, because all our subsequent processing is based on the array obtained after flattening.

After flattening, all nodes become the items of the outermost array (originally, only level 1 nodes are the items of the outermost array).

const flatNodes = [
    {
        key: '0-0',
        children: [
            {
                key: '0-0-0'
            },
            {
                key: '0-0-1'
            }
        ]
    },
    {
        key: '0-0-0'
    },
    {
        key: '0-0-1'
    },
    {
        key: '0-1',
        children: [
            {
                key: '0-1-0'
            },
            {
                key: '0-1-1'
            }
        ]
    },
    {
        key: '0-1-0'
    },
    {
        key: '0-1-1'
    }
];

At this time, if we traverse the array and draw it on the page (i.e. list rendering), the order of the nodes obtained is the same as that when rendering the original data into a tree, but the former is not indented and looks like a list, while the latter is indented and looks like a tree, as shown in the following two figures.

3.2 calculate which data is in the visual area

In fact, we need to process a lot of data, and we only need to render part of it, that is, the part in the visual area, so we need to calculate which data is in the visual area.

The height of the visual area is fixed. Assuming that the height of each node of the tree is also fixed, we can calculate how many nodes can be displayed in the visual area according to these two data.

// VISIBLE_HEIGHT the height of the visible area
// NODE_HEIGHT of the node of the height tree
// visibleCount how many nodes can be displayed in the visual area
const visibleCount = Math.ceil(VISIBLE_HEIGHT / NODE_HEIGHT);

Then, if we know the index of the first node in the visual area in the array, we can know which data is in the visual area.

The index of the first node in the visual area can be obtained by dividing the current rolling distance by the node height.

// scrollTop current scrolling distance (that is, the distance from the current position to the top of the page)
// start the index of the first node of the visual area
let start = Math.floor(scrollTop / NODE_HEIGHT));

Now we know which data is in the visual area.

let visibleNodes = flatNodes.slice(start, Math.min(start + visibleCount, flatNodes.length));

3.3 simulated rolling

Because we actually only render the data of the visual area, and if we only have these data, we can't browse all the data by scrolling the page like when rendering all the data, so we also need to simulate scrolling.

After simulating the scrolling, the data of the visual area will be dynamically modified with the scrolling, and you can browse all the data by scrolling the page like when rendering all the data.

To simulate scrolling, we need this HTML structure:

<div class="virtual-tree" style="height: 500px; overflow: scroll; position: relative;" on-scroll="scrollEvent">
    <div class="tree-phantom" style="height: {{totalHeight}}px;"></div>
    <ul class="visible-tree-nodes" style="position: absolute; top: {{top}}px">
        <s-tree-node s-for="node in visibleNodes" nodeData="{{node}}"></s-tree-node>
    </ul>
 </div>

s-tree-node is a tree node component. The data passed into the tree node can render the tree node.

The outermost element (class: virtual tree) is a scroll container, and overflow: scroll; is set;, It is also a visible area with a fixed height.

The first child element of the rolling container (class tree phantom) is the key to simulating rolling. It is a placeholder element. The height is the total height of the tree, which is obtained by multiplying the node height by the number of nodes. This placeholder opens the scroll container with the true height of the tree, so the scroll container can scroll.

The second child element of the scroll container (class is visible tree nodes), which we call the rendering element, is responsible for rendering the data we calculated in the visual area. It is positioned relative to the rolling container (the outermost element), and the initial position is at the top of the rolling container. As the container scrolls, we not only need to update the data of the visual area as mentioned earlier, but also need to update the vertical offset of the rendering element, that is, the value of top in the above code. Otherwise, the data that should be displayed in the visual area will run out of the visual area.

// The event function triggered when scrolling is responsible for updating the data of the visual area and updating the vertical offset of the rendering element
scrollEvent() {
    // Current scroll position
    const scrollTop = ducument.querySelector('virtual-tree').scrollTop;
    // Update the index of the first node of the visible area
    // In a real implementation, visibleNodes (the data of the visual area) is a calculated attribute, so it will be automatically updated as start (the index of the first node of the visual area) is updated
    this.data.set('start', Math.floor(scrollTop / NODE_HEIGHT));

    // Update the offset of the rendered element
    this.data.set('top', scrollTop - (scrollTop % NODE_HEIGHT));
}

At this point, the simulated scrolling is complete.

3.4 decorate the list as a tree

At this step, we have already realized the basic functions of the virtual tree, but the tree is essentially a list. For example, there is no hierarchical indentation (as shown earlier), so we need to decorate the list as a tree.

The main work of this step is to write CSS, which is not the focus of today, so let's skip it ~

4 final effect

Finally, with 11110 nodes, let's take a look at the rendering time when using the virtual tree:


0.19 seconds, 99% faster than the original 26 seconds.

Quite shocking.

The function of this virtual Tree has been implemented in the Tree component of Santd. Interested friends can go to the official documentation and code base of Santd to learn more:

  1. Advanced front end high performance rendering 100000 pieces of data (virtual list): https://juejin.cn/post/684490...
  2. Virtual list / tree: https://ldc4.github.io/blog/v...
  3. How to implement a high-performance Tree component that can render big data: https://cloud.tencent.com/dev...

Click to enter for more technical information~~

Posted by HTTP-404 on Tue, 09 Nov 2021 02:58:14 -0800