Angular: How to use Virtual Scroll with a Flat Tree?
While developing OctoPerf's frontend I quickly stumbled upon performance issues with Angular Material tree when too many nodes where opened.
OctoPerf is a load testing solution.
As such, it displays a tree of actions and containers used to script the load testing scenarios:
With hundreds of directories expanded, the frontend quickly became slow to the point of being unusable.
By default, even nodes outside of the visible part of the tree are rendered in the DOM.
Since this feature is not directly available yet, I had to switch from a classic CSS scrollbar to a Virtual Scroll manually.
This blog post lists all the steps I had to follow in order to use a Virtual Scroll on a complex Material Tree.
Note:
The actions tree in octoPerf uses a FlatTreeControl to manage the data. It is probably quite harder (or even impossible) to use a Virtual Scroll with a NestedTreeControl!
The cdk-virtual-scroll-viewport component specifies the itemSize (the height of our tree nodes in our case),
The structural directive *cdkVirtualFor loops over the tree dataSource.
<cdk-virtual-scroll-viewportitemSize="26"minBufferPx="400"maxBufferPx="800"><ng-container*cdkVirtualFor="let node of dataSource"><div[style.padding-left]="depth(node)*20+'px'"class="app-tree-node"><lib-storage-node[node]="node"[expanded]="treeControl.isExpanded(node)"></lib-storage-node></div></ng-container></cdk-virtual-scroll-viewport>
Since we are not in a tree anymore, we cannot use dedicated directives such as matTreeNodePadding to display the indentation of the nodes.
Here it is replaced by a simple style binding [style.padding-left]="depth(node) * 20 + 'px'".
The depth is computed in the component and is basically the depth of the current node minus the depth of the root node.
This will obviously depend on what kind of nodes your tree is displaying.
Also, you may have to make a few css adjustment to enforce the height of your nodes, here with the app-tree-node class:
The maxBufferPx and minBufferPx inputs sets the buffer size of the virtual scroll in pixels. I had to increase these values (100 to 200px being the default) in order to avoid displaying a clear tree when the user quickly scrolls through it.
The OctoPerf actions tree uses another component to display the files/directory nodes: lib-storage-node.
In the previous version, the toggle button lib-toggle-button was added before the node component.
I could not manage to make this work with the Virtual Scroll and decided to move the toggle button into the node.
The Virtual Scroll does View Recycling by default to improve rendering performance.
The size of the view cache can be adjusted via the templateCacheSize property and setting it to 0 disables caching.
Since we are trying to improve UI performance that might be a bad idea.
So we have to take extra care to handle this view recycling cache.
The recycling of views implies a few modifications on the tree node component.
Here is the previous version of the tree node:
It is a simple component. It displays an icon and a name for each node using pipes (storageNodeToIcon and storageNodeToName).
Buttons are injected using a portal and displayed on mouse over (More about this).
Compared to the Virtual Scroll one, the HTML is quite similar:
The toggle button to expand/collapse directories was moved from the tree component to the node component (More about this).
The major update to do is on the component itself.
In the MatTree version, the node data and expanded state are injected directly on class fields, and the buttons CdkPortal is set in the ngOnInit method:
ngOnInit is not called when the component view is recycled.
So to prevent visual glitches such as nodes having the buttons displayed or files (tree leaves) with a toggle button, the node data is injected via a setter:
As the logic to display a toggle button only for directories was moved from the tree component to the node component (the code feels clearer this way!), the node setter updates the hasChild property.
This does the trick but all buttons are present in the DOM even when not displayed.
To improve the global performance of the tree it is better to completely remove the buttons from the view when the user has not his mouse over the tree node.
So I dropped the CSS classes in favor of a boolean property named hover on the component:
One big advantage of the Virtual Scroll is its capacity to scroll to a selected index.
In OctoPerf, when an action is selected in the center pane, is it automatically selected in the files tree on the left (like in any IDE).
In the previous version I had to implement the logic to automatically scroll to the selected tree node and its quite complex to manage.
With the CdkVirtualScrollViewport you can simply call the scrollToIndex method: