Fix slow appearance of nodes on tree view

The tree view rendering performance is optimized by improving the node
render queue ordering. The node rendering order is modified based on the
expansion state and the depth in the hierarchy, leading to faster
rendering of visible nodes. This optimization is applied when the tree
nodes are not expanded to improve the rendering speed.

This new ordering ensures that nodes are rendered more efficiently,
prioritizing nodes that are collapsed and are at a higher level in the
hierarchy.
This commit is contained in:
undergroundwires
2023-09-25 14:21:29 +02:00
parent 8f188acd3c
commit bd2082e8c5
13 changed files with 205 additions and 19 deletions

View File

@@ -51,7 +51,7 @@ import {
} from 'vue';
import { TreeRoot } from '../TreeRoot/TreeRoot';
import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
import { NodeRenderingStrategy } from '../Rendering/NodeRenderingStrategy';
import { NodeRenderingStrategy } from '../Rendering/Scheduling/NodeRenderingStrategy';
import { useNodeState } from './UseNodeState';
import { TreeNode } from './TreeNode';
import LeafTreeNode from './LeafTreeNode.vue';

View File

@@ -0,0 +1,29 @@
import { ReadOnlyTreeNode } from '../../Node/TreeNode';
import { RenderQueueOrderer } from './RenderQueueOrderer';
export class CollapseDepthOrderer implements RenderQueueOrderer {
public orderNodes(nodes: Iterable<ReadOnlyTreeNode>): ReadOnlyTreeNode[] {
return orderNodes(nodes);
}
}
function orderNodes(nodes: Iterable<ReadOnlyTreeNode>): ReadOnlyTreeNode[] {
return [...nodes]
.sort((a, b) => {
const [aCollapseStatus, bCollapseStatus] = [isNodeCollapsed(a), isNodeCollapsed(b)];
if (aCollapseStatus !== bCollapseStatus) {
return (aCollapseStatus ? 1 : 0) - (bCollapseStatus ? 1 : 0);
}
return a.hierarchy.depthInTree - b.hierarchy.depthInTree;
});
}
function isNodeCollapsed(node: ReadOnlyTreeNode): boolean {
if (!node.state.current.isExpanded) {
return true;
}
if (node.hierarchy.parent) {
return isNodeCollapsed(node.hierarchy.parent);
}
return false;
}

View File

@@ -0,0 +1,5 @@
import { ReadOnlyTreeNode } from '../../Node/TreeNode';
export interface RenderQueueOrderer {
orderNodes(nodes: Iterable<ReadOnlyTreeNode>): ReadOnlyTreeNode[];
}

View File

@@ -1,4 +1,4 @@
import { TreeNode } from '../Node/TreeNode';
import { TreeNode } from '../../Node/TreeNode';
export interface NodeRenderingStrategy {
shouldRender(node: TreeNode): boolean;

View File

@@ -1,4 +1,4 @@
import { DelayScheduler } from './DelayScheduler';
import { DelayScheduler } from '../DelayScheduler';
export class TimeoutDelayScheduler implements DelayScheduler {
private timeoutId: ReturnType<typeof setTimeout> | undefined = undefined;

View File

@@ -1,13 +1,15 @@
import {
WatchSource, computed, shallowRef, triggerRef, watch,
WatchSource, shallowRef, triggerRef, watch,
} from 'vue';
import { ReadOnlyTreeNode } from '../Node/TreeNode';
import { useNodeStateChangeAggregator } from '../UseNodeStateChangeAggregator';
import { TreeRoot } from '../TreeRoot/TreeRoot';
import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
import { NodeRenderingStrategy } from './NodeRenderingStrategy';
import { NodeRenderingStrategy } from './Scheduling/NodeRenderingStrategy';
import { DelayScheduler } from './DelayScheduler';
import { TimeoutDelayScheduler } from './TimeoutDelayScheduler';
import { TimeoutDelayScheduler } from './Scheduling/TimeoutDelayScheduler';
import { RenderQueueOrderer } from './Ordering/RenderQueueOrderer';
import { CollapseDepthOrderer } from './Ordering/CollapseDepthOrderer';
/**
* Renders tree nodes gradually to prevent UI freeze when loading large amounts of nodes.
@@ -19,6 +21,7 @@ export function useGradualNodeRendering(
scheduler: DelayScheduler = new TimeoutDelayScheduler(),
initialBatchSize = 30,
subsequentBatchSize = 5,
orderer: RenderQueueOrderer = new CollapseDepthOrderer(),
): NodeRenderingStrategy {
const nodesToRender = new Set<ReadOnlyTreeNode>();
const nodesBeingRendered = shallowRef(new Set<ReadOnlyTreeNode>());
@@ -28,8 +31,6 @@ export function useGradualNodeRendering(
const { onNodeStateChange } = useChangeAggregator(treeWatcher);
const { nodes } = useTreeNodes(treeWatcher);
const orderedNodes = computed<readonly ReadOnlyTreeNode[]>(() => nodes.value.flattenedNodes);
function updateNodeRenderQueue(node: ReadOnlyTreeNode, isVisible: boolean) {
if (isVisible
&& !nodesToRender.has(node)
@@ -47,14 +48,15 @@ export function useGradualNodeRendering(
}
}
watch(() => orderedNodes.value, (newNodes) => {
watch(() => nodes.value, (newNodes) => {
nodesToRender.clear();
nodesBeingRendered.value.clear();
if (!newNodes?.length) {
if (!newNodes || newNodes.flattenedNodes.length === 0) {
triggerRef(nodesBeingRendered);
return;
}
newNodes
.flattenedNodes
.filter((node) => node.state.current.isVisible)
.forEach((node) => nodesToRender.add(node));
beginRendering();
@@ -80,10 +82,8 @@ export function useGradualNodeRendering(
return;
}
isRenderingInProgress = true;
const sortedNodes = Array.from(nodesToRender).sort(
(a, b) => orderedNodes.value.indexOf(a) - orderedNodes.value.indexOf(b),
);
const currentBatch = sortedNodes.slice(0, batchSize);
const orderedNodes = orderer.orderNodes(nodesToRender);
const currentBatch = orderedNodes.slice(0, batchSize);
if (currentBatch.length === 0) {
return;
}

View File

@@ -22,7 +22,7 @@ import {
} from 'vue';
import HierarchicalTreeNode from '../Node/HierarchicalTreeNode.vue';
import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
import { NodeRenderingStrategy } from '../Rendering/NodeRenderingStrategy';
import { NodeRenderingStrategy } from '../Rendering/Scheduling/NodeRenderingStrategy';
import { TreeRoot } from './TreeRoot';
export default defineComponent({