T
TanStack2y ago
evident-indigo

Heavy CPU usage on virtualized tables?

Steps to reproduce: 1. Visit this codesandbox link (derived from https://tanstack.com/virtual/v3/docs/examples/react/fixed): https://f2mml2-5173.csb.app/ 2. In Chrome, open the Performance Monitor (Inspect Element -> Three dots icon at the top right -> "Open Drawer" -> in the opened drawer, show the "Performance Monitor" tab) 3. Scroll very fast in the third example (the table with cells) of the sandbox example using a mouse wheel. Notice the CPU spike. In the first example with plain rows, the CPU spike is very minimal. I have attached a call tree to this post showing which function is taking up the most time in tanstack virtual, and it appears to be an onChange function. That function can be seen here: https://github.com/TanStack/virtual/blob/62d6dc8cf917e8500b4709a59a46f936d9252f61/packages/react-virtual/src/index.tsx#L31 I have a couple questions: 1. Is this performance loss expected? 2. Is there any way to minimize the CPU usage here?
React Virtual Fixed): Example | TanStack Virtual Docs
An example showing how to implement Fixed): in React Virtual
GitHub
virtual/packages/react-virtual/src/index.tsx at 62d6dc8cf917e8500b4...
🤖 Headless UI for Virtualizing Large Element Lists in JS/TS, React, Solid, Vue and Svelte - TanStack/virtual
No description
11 Replies
evident-indigo
evident-indigoOP2y ago
Looking deeper into the issue, it appears to just be the flushSync usage on every scroll. I think flushSync may not be the appropriate choice here, per the docs it should be used sparingly: https://react.dev/reference/react-dom/flushSync flushSync usage was added to resolve this issue: https://github.com/TanStack/virtual/issues/549 Seems like it fixes the scrolling, but with the caveat of high CPU usage. With data-heavy rows, the CPU usage is even more pronounced (upwards of 50%+ on scroll alone). Would like to help push for an alternative solution that addresses both issues
flushSync – React
The library for web and native user interfaces
GitHub
Running virtual examples with React 18 createRoot causes rendering ...
Describe the bug I have tested the examples/react/fixed example with React 18, so changed the init code to use the new createRoot / render API: ReactDOM.render( , document.getElementById('root&...
ambitious-aqua
ambitious-aqua2y ago
I'm developing (and testing) on a ~9 year old pc and I also have a bit of a lag in nearly every virtual scrolling implementation, if the row components are a bit heavier. Also I noticed that the final production build runs vastly better than in dev mode. Not sure how, but react-virtuoso had a the better performance on my machine when scrolling really fast (feels like they skip some rendering calls when scrolling super-fast). My final choice was tanstack virtual because of the API and also the dynamic sizing of the scrolling container. For performance optimizations I did memoization and added placeholders for very complex list entries. On modern machines (tested on a notebook) the virtual scrolling now feels like native, and on this machine it's a good to OK UX I'd say. Also I'm using Chakra, which isn't too performance friendly for a lot of re-mounts and renderings, like you have it in a virtual scrolling use-case.
unwilling-turquoise
unwilling-turquoise2y ago
Would like to help push for an alternative solution that addresses both issues
@Boston would be great, do you have any ideas how we can tackle this? The performance is complex topic @NiklasPor as you noticed, it's not one thing but most cases many.
evident-indigo
evident-indigoOP2y ago
I am fairly new to using tanstack libs, been enjoying using them so far. Diving into the library internals for the first time here, so I don't have contextual info about what the goal of the flushSync call site is. Do we know what is the root cause of the scrolling lag issue that flushSync had solved? Because then we can start from there 👍 Doing a bit more research: It looks like with React 18 and the introduction of concurrent rendering, the "flickering" bwamsellem had discovered in github issue #549 appears to be caused by React dropping renders at its own discretion. I would like to point out that in bwamsellem's demo (found here: https://react-ts-mfeisb.stackblitz.io/), scrolling appears smooth when using the mouse wheel, but when scrolling using the scrollbar, that is where the flickering issue arises. The root cause of this is that scroll events are able to fire at a much faster rate than react renders. So much so in fact that React 18's new concurrent rendering is dropping renders in order to keep performance high. In this case, flushSync solves the issue by forcing React to dump queued render updates and to forcefully rerender the virtual DOM tree. A potential solution would be not to remove flushSync altogether, but to reduce the rate at which it is called: 1. Limit its invocations to 60fps (in other words, throttle the scroll events triggering flushSync to be inline with the number of renders). 2. Only call flushSync when the user is moving at a rapid rate with the scroll bar (using scrollTop/scrollLeft and the clientHeight/clientWidth, we can compute the delta of the user's scrolling, and if the user's scroll delta reaches a certain rate, such as the delta being greater than the clientHeight/clientWidth of the table, that is when we should flushSync, because we know at that point the scrolling occurs faster than React's concurrent rendering can tolerate). With these two changes, the CPU usage on mouse wheel scrolls should drop to previous levels, while high-frequency scrolls with the scrollbar should be able to keep up with rendering, while also reducing CPU usage EDIT: It might be possible to remove flushSync altogether with just the throttling of scrolling-based rerenders limited to 60fps
conscious-sapphire
conscious-sapphire2y ago
I've been seeing the same issues. Any thoughts on this? @piecyk
unwilling-turquoise
unwilling-turquoise2y ago
Great work @Boston basic we can wrap scroll event with rAF and see how it works, this can be tested without changes in core by providing observeElementOffset, like here https://codesandbox.io/p/devbox/still-sound-76nsps?embed=1&file=%2Fsrc%2Fmain.tsx%3A36%2C1
CodeSandbox
CodeSandbox brings instant cloud development environments that keep you in flow.
unwilling-turquoise
unwilling-turquoise2y ago
also @Jacob checkout the observeElementOffset from codesandbox
conscious-sapphire
conscious-sapphire2y ago
Am I going insane, or does that CodeSandbox not have any code files I can view? 😂
No description
conscious-sapphire
conscious-sapphire2y ago
Weird, I used this link instead and it worked: https://codesandbox.io/p/devbox/still-sound-76nsps
unwilling-turquoise
unwilling-turquoise2y ago
just as refernce
const observeElementOffset = <T extends Element>(
instance: Virtualizer<T, any>,
cb: (offset: number) => void,
) => {
const element = instance.scrollElement;
if (!element) {
return;
}

let ticking = false;

const handler = () => {
ticking = false;
cb(element[instance.options.horizontal ? "scrollLeft" : "scrollTop"]);
};

function requestTick() {
if (!ticking) {
requestAnimationFrame(handler);
}
ticking = true;
}

handler();

element.addEventListener("scroll", requestTick, {
passive: true,
});

return () => {
element.removeEventListener("scroll", requestTick);
};
};
const observeElementOffset = <T extends Element>(
instance: Virtualizer<T, any>,
cb: (offset: number) => void,
) => {
const element = instance.scrollElement;
if (!element) {
return;
}

let ticking = false;

const handler = () => {
ticking = false;
cb(element[instance.options.horizontal ? "scrollLeft" : "scrollTop"]);
};

function requestTick() {
if (!ticking) {
requestAnimationFrame(handler);
}
ticking = true;
}

handler();

element.addEventListener("scroll", requestTick, {
passive: true,
});

return () => {
element.removeEventListener("scroll", requestTick);
};
};
Yo yo, did someone had time to play around with throttle scroll events, any comments? Issue is that it's really hard to measure the performance here
evident-indigo
evident-indigoOP2y ago
I have forked the original codesandbox app (original found here: https://f2mml2-5173.csb.app/) and experimented with observeElementOffset on that same demo, but I have not noticed any noticeable improvements with observeElementOffset applied to it, unfortunately. flushSync is still causing the same performance impact. Note that I have only included the grid example on the fork, as the CPU usage spike is easier to profile with tables containing more DOM elements. here is the fork I had made: https://codesandbox.io/p/devbox/blissful-wildflower-qlq4ht Separate link: https://qlq4ht-5173.csb.app/ (Please let me know if any of these links do not work)

Did you find this page helpful?