headless-tree
headless-tree copied to clipboard
Questions about performance in large trees while dragging
I've been using react-complex-tree for a while and have run into some issues with performance. It seems that with a sufficiently large tree, frames can start to drop when certain operations, like mouse drags, begin.
It seems to largely be due to item re-renders occurring on drag mouse movement for all items in the tree, not just those that are affected by the drag in progress. I forked up a little demo starting from the react-complex-tree demo:
- go to https://codesandbox.io/p/devbox/react-complex-tree-playground-forked-zmx58x (and fork it)
- in
index.tsx, change the App you're importing toreact-complex-tree.tsx - open browser console
- drag something around in the tree and watch the re-render count skyrocket
After reading through the source of RCT I came to the conclusion that there wasn't anything I could do to avoid these full tree re-renders. So I turned my attention to this headless-tree project to see where we're at in comparison. I migrated the "Big Tree" storybook demo over to codesandbox demo to count the re-renders.
So following the above steps again (but using App from headless-tree.tsx) we can see in the console output that we're re-rendering the whole tree again during every frame of the mouse drag.
- Is this unavoidable? Is there something I can do differently in either of these demos to keep the dragging features without the re-renders of the unaffected items?
- Is this expected behavior of either or both libraries?
Hi @david-summation, thanks for your input! This is an important question regarding performance, that I have often wondered what the best approach is. The gist is that full rerenders is to be expected by default, but can be mitigated with proper memoization, and Headless Tree provides you with additional capabilities to improve on that.
The general issue with this is, that I as library author want to make as few assumptions about how library consumers implement their rendering logic for individual tree items or subsections of the tree. Theoretically it could be said that changes to a substructure of the tree should not trigger rerenders for unrelated parts of the tree, but I found that determining what is a related section of a tree and which parts of a tree state is relevant to what certain parts of the tree consider relevant for their rendering behavior is a non-trivial rabbit hole, where it's hard to find a one-fits-all solution that is easy enough to understand as user, doesn't blow up bundle size and still retains as many use cases as possible.
The proper solution is to make use of proper memoization of tree items in cases where render effort is not somewhat trivial for each tree item. That was already possible in RCT, in Headless Tree I've written some dedicated documentations on that matter: https://headless-tree.lukasbach.com/recipe/handling-expensive-components
In most situations, this is enough to entirely mitigate the issue, since most of the rendering overhead comes from actually rendering all tree items, and by memoizing them in such a way that their rerender is not triggered when unrelated properties change, should cut down on the majority of rerendering triggers, while running the hook itself that manages the tree will cause a mostly neglectible performance overhead.
There is another thing to keep in mind however. Rendering very very large trees can still cause noticable performance issues, this can even happen without the rerenders or in cases of proper memoization due to the general render overhead multiplied by many items rendered. This is more of a general web dev issue causes by rendering many things at once, not something specific to Headless Trees. Chrome already throws warnings for more than 800 DOM nodes in total on a web page (not number of childrens per parent, but absolute total nodes on a document). The mitigation for this is to use virtualization libraries, that make sure that only those items are rendered that are currently visible to the user, and while Headless Tree doesn't provide virtualization out of the box, it is supported to be used with established virtualization libaries like Tanstack Virtual or React Window. I found that both general tree use and drag-and-drop works fine in virtualized cases with Headless Tree for at least a few 100k items in a tree, and the rerender topic becomes a very minor topic since only 20-30 items will be rendered visually at any given time.
Since I don't see any actionable next steps for this, I'll close this for now. Please reopen or create a new ticket if something else comes up. Thanks!