Skip to content

Commit f2377b7

Browse files
committed
perf(webapp): replace per-row tree scans with id lookup maps in TreeView
The virtualizer render path ran tree.find per virtual row and getNodeProps ran tree.findIndex per rendered node, which is quadratic work on large traces. Both now resolve through memoized id-to-index maps with identical behavior.
1 parent f4bb92f commit f2377b7

1 file changed

Lines changed: 32 additions & 4 deletions

File tree

apps/webapp/app/components/primitives/TreeView/TreeView.tsx

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import { VirtualItem, Virtualizer, useVirtualizer } from "@tanstack/react-virtual";
22
import { motion } from "framer-motion";
3-
import { MutableRefObject, RefObject, useCallback, useEffect, useReducer, useRef } from "react";
3+
import {
4+
MutableRefObject,
5+
RefObject,
6+
useCallback,
7+
useEffect,
8+
useMemo,
9+
useReducer,
10+
useRef,
11+
} from "react";
412
import { cn } from "~/utils/cn";
513
import { NodeState, NodesState, reducer } from "./reducer";
614
import { concreteStateFromInput, selectedIdFromState } from "./utils";
@@ -47,6 +55,16 @@ export function TreeView<TData>({
4755

4856
const virtualItems = virtualizer.getVirtualItems();
4957

58+
// id -> node lookup so each virtual row resolves in O(1) instead of
59+
// scanning the whole tree per row.
60+
const nodesById = useMemo(() => {
61+
const map = new Map<string, FlatTreeItem<TData>>();
62+
for (const node of tree) {
63+
map.set(node.id, node);
64+
}
65+
return map;
66+
}, [tree]);
67+
5068
const scrollCallback = useCallback(
5169
(event: Event) => {
5270
if (!onScroll) return;
@@ -99,7 +117,7 @@ export function TreeView<TData>({
99117
}}
100118
>
101119
{virtualItems.map((virtualItem) => {
102-
const node = tree.find((node) => node.id === virtualItem.key);
120+
const node = nodesById.get(virtualItem.key as string);
103121
if (!node) return null;
104122
const state = nodes[node.id];
105123
if (!state) return null;
@@ -197,6 +215,16 @@ export function useTree<TData, TFilterValue>({
197215
concreteStateFromInput({ tree, selectedId, collapsedIds, filter })
198216
);
199217

218+
// id -> index lookup so getNodeProps resolves in O(1) instead of scanning
219+
// the whole tree per rendered row.
220+
const treeIndexById = useMemo(() => {
221+
const map = new Map<string, number>();
222+
tree.forEach((node, index) => {
223+
map.set(node.id, index);
224+
});
225+
return map;
226+
}, [tree]);
227+
200228
//sync external selectedId prop into internal state
201229
useEffect(() => {
202230
const internalSelectedId = selectedIdFromState(state.nodes);
@@ -497,7 +525,7 @@ export function useTree<TData, TFilterValue>({
497525
(id: string) => {
498526
const node = state.nodes[id];
499527
if (!node) return {};
500-
const treeItemIndex = tree.findIndex((node) => node.id === id);
528+
const treeItemIndex = treeIndexById.get(id) ?? -1;
501529
const treeItem = tree[treeItemIndex];
502530
return {
503531
"aria-expanded": node.expanded,
@@ -506,7 +534,7 @@ export function useTree<TData, TFilterValue>({
506534
tabIndex: node.selected ? -1 : undefined,
507535
};
508536
},
509-
[state]
537+
[state, treeIndexById]
510538
);
511539

512540
return {

0 commit comments

Comments
 (0)