Sluggish scroll on chromium based browsers when using many columns
Describe the bug
Trying to render a Tanstack table with around 50 columns and I want to avoid using column virtualization and just stick with row virtualization. Seems like in Edge/Chrome I am getting pretty low FPS (~20 fps or so) once I start adding more and more columns. Weird thing is that this is even the case when I use a small dataset (200 rows) and overscan all of the items. On Firefox however I am getting good performance (~60fps).
Also I noticed that if I convert my code to useWindowVirtualizer() the scrolling performs way better, but unfortunately that doesn't suit my use case.
Your minimal, reproducible example
https://stackblitz.com/edit/tanstack-virtual-2gupur?file=src%2Fmain.tsx
Steps to reproduce
- add 20+ columns to the table (I did already in the StackBlitz
- Start scrolling in a chromium based browser
Expected behavior
- 60fps-ish scrolling
How often does this bug happen?
Every time
Screenshots or Videos
No response
Platform
Windows 11, Edge v130
tanstack-virtual version
v3.10.8
TypeScript version
No response
Additional context
No response
Terms & Code of Conduct
- [x] I agree to follow this project's Code of Conduct
- [x] I understand that if my bug cannot be reliable reproduced in a debuggable environment, it will probably not be fixed and this issue may even be closed.
Thanks for reporting this! I’ll check into the issue soon and see what can be done to improve the performance. Appreciate your patience!
I'm facing the same problem
In my use case, I could solve the scroll lag when I added the following to the table container:
.container {
// ...
contain: 'paint',
will-change: 'transform',
}
In my use case, I could solve the scroll lag when I added the following to the table container:
.container { // ... contain: 'paint', will-change: 'transform', }
Setting the will-change: transform works for me as well, thank you for sharing!
@piecyk It would be great if the Tanstack team could figure out why the CSS property is necessary? For me, the lagging only occurred after I revised and refactored our table components. So while it worked smoothly before, I now need this CSS property. That's strange.
@MatchuPitchu That's interesting! It seems like something might have changed in Chrome recently. When I tested will-change: transform before, I didn’t notice much difference, but I’ll look into it again. Also, another CSS property that can help with virtualization issues is overflow-anchor: none; it can sometimes prevent unexpected scroll. I’ll experiment with these and let you know if I find anything useful!
Thank you. During the test I noticed that the lagging in Edge was much less, even without the CSS property. This confirms that it could have something to do with Chrome.
Do you mean or on the items?
.container {
overflow-anchor: none;
}
@MatchuPitchu directly on each item.
@Piecyk, I’m still experiencing sluggish scrolling when using the scrollbar. However, when I scroll using the Shift key combined with the arrow keys, the issue is significantly less noticeable.
@soliyapintu facing same issue. @piecyk any solution?
@piecyk my issues still persists, I have like 20 FPS on scrolling and elements load with delay
@soliyapintu @vansh3476 @wwesolowski just to be on same page, basic combining react-table with react-virtual everyone experiencing these problems?
@piecyk Unfortunately, yes. The scrolling behavior is smooth in Firefox and Edge, but it is sluggish in Chrome. Additionally, I have sticky columns on both the left and right sides. Does react-virtual provide any functionality to handle sticky columns?
@piecyk Yes facing same problem.
@piecyk tanstack table + tanstack virtual
In my case, maybeNotify takes more than 1300 ms
In my case, maybeNotify takes more than 1300 ms
+1 Same problem
+1, extremely laggy scrolling on Chromium-based browsers, but it seems like there is no issue at all on Firefox-based ones.
I can also confirm that putting the contain-paint and will-change-transform Tailwind classes on my table container somewhat fixes the issue — though I feel like Firefox somehow still has superior performance, Chrome is still "lagging" behind with render performance, however, the scrolling performance is MUCH better.
thank you the style={{ contain: 'paint', willChange: 'transform'}} did the trick
Hi! I have this problem too, but I made scrolling a little faster by using smooth scrolling, which is described in the TanStack documentation
After extensive debugging and analysis, I observed that rendering simple text resolves the lagging issue.
I significantly improved our table's performance by wrapping the virtualized absolute div children with React.memo. This optimization reduces unnecessary re-renders, enhancing efficiency.
Before memoization :- https://github.com/TanStack/virtual/issues/860#issuecomment-2500166012 After memoization :-
I significantly improved our table's performance by wrapping the virtualized absolute div children with React.memo. This optimization reduces unnecessary re-renders, enhancing efficiency.
Before memoization :- #860 (comment) After memoization :-
Tell me please. Do you have an example, how you did it?
@YuesIt17 You need to take extra div above <tr /> tag and you need to give all required virtulization styles to that div and inside that you need to render original row and need to wrap into React.memo. Make sure your <tr /> should not re-render when virtulization styles and props changes."
@soliyapintu can you provide any codesandbox example of fixing the issue?
@piecyk Im have same problem. Hi, do you have any solutions?
We are struggeling with this issue for a while. We have a rather complex data table with a good amount of subnodes.
As I've been working on this problem, I've come to the understanding that there are multiple pitfalls:
- if you're layouting your table with
transformto position the virtualized items, then you should wrap the element with a outside style receiver or ref-logic to modify the transform property directly - your virtualized items create a lot of node pressure when scrolling really fast (if you pull on the scroll-bar you're entering a lot of react nodes into the DOM, all which pull down performance significantly:
- React Devtools + Strict Mode are major performance downgrades, which has to be kept in mind.
Our current approach is like that:
// the forward-ref is important to make positioning work
const MemoizerRow = forwardRef<HTMLDivElement, { index: number; className?: string; style?: CSSProperties | undefined; children?: ReactNode }>(
function MemoizerRow({ index, className, style, children }, ref) {
return (
<div className={cx('_row', memoizerClass, className)} ref={ref} data-index={index} style={style}>
{children}
</div>
)
},
)
// memo is important here, such as the actual discrete row does not get rebuilt
export const DetailsVariantRow: FC<{ entry: VariantDetailsRow; context: DetailsContext }> = memo(function DetailsVariantRow({ entry, context }) {
// ...
return (
<div>
{ /* and all the things you need */ }
</div>
)
})
To mitigate JS runtime pressure, I'm currently working on a solution to measure the problem, but a simple PoC improves this already:
const VirtualizedGrid: FC< items: MyItem[] > = ({ items }) => {
const listRef = useRef<HTMLDivElement>(null)
const getScrollElement = useCallback(() => listRef.current, [])
const virtualizer = useVirtualizer({
count: items.length,
estimateSize: (idx) => estimateSizeImpl(items[idx]),
overscan: 5,
getScrollElement,
paddingEnd: 300,
rangeExtractor,
enabled: !isPrint,
onChange: (instance) => setVisible(10),
})
const [visible, setVisible] = useState(10)
useEffect(() => {
// increment every frame the amount of visible nodes until the screen is filled
const func = () => {
setVisible((curr) => {
if(curr > virtualizer.getVirtualItems().length) {
return curr;
}
return curr + 5 } )
timer = requestAnimationFrame(func)
}
let timer = requestAnimationFrame(func)
return () => cancelAnimationFrame(timer)
}, [virtualizer])
}
All that's now missing is profiling the rendering pressure between frames (so you can dynamically change the display window) and have a virtualizer local lookup map which items have been rendered already and are ready from the memo cache, so that visible only counts new created items.
Another possible improvement is keeping a cache of rows that have been in view once, such that they can be restored even when they have been removed from the DOM by the virtualizer.
With that I get reasonable ~120fps even on full screen scrolls.
Edit: I also think that this has little to do with Firefox or Edge, but people forgetting that they haven't installed React Dev Tools on these browsers. Additionally Firefox' rendering strategy feels smoother, because the child layout is calculated asynchronously, while the node appearance (first draw of a virtualized node) is roughly in the same ballpark. Just that the window (and scroll) responsiveness doesn't degrade as a bad as with Chromium based browsers.
Another edit: Patching the onChange handler to not use flushSync actually improves my rendering performance by roughtly 2x:
const virtualizer = useVirtualizer({
count: displayData.length,
estimateSize: (idx) => estimateSizeImpl(displayData[idx]),
overscan: 5,
getScrollElement,
paddingEnd: 300,
rangeExtractor,
enabled: !isPrint,
onChange: (instance) => {
if (instance.scrollDirection) {
insertionDirection.current = instance.scrollDirection
}
},
})
const rerender = useReducer(() => ({}), {})[1]
// this small change gives a 2x performance, cool, ey?
virtualizer.options.onChange = (instance) => {
rerender()
}
Thank you for all your thoughts @mio-moto . Do you have a full code example in which your optimizations can be better understood? I don't understand your onChange patching. For example, where does insertionDirection.current come from and what do you do with this ref?
@MatchuPitchu I have a working example here.
Be sure to click the "Open preview in new tab" button, otherwise React Scan won't show up. With that, you can compare the performance on your machine.
Speedy
https://stackblitz.com/edit/tanstack-virtual-cdrheilm
https://github.com/user-attachments/assets/1a9af18a-a784-4681-b64f-96ced6d49a71
(Watch the FPS counter)
Sluggish
https://stackblitz.com/edit/tanstack-virtual-k17x9n25
https://github.com/user-attachments/assets/2bd56311-8a8d-49cc-aec4-ac40a36dc5ee
(Watch the FPS counter)
Some notes
- roughly half the pressure comes from the layouter/compositor, which is due to this being a grid and rows inheriting from that. We have that in production like that and it's working "good enough", with the major benefit to not doing all the layouting calculation yourself.
- all the rows that have been displayed in the current scroll are cached through memoization
- I've toyed plenty with keeping a cache of all rows, but the primary pressure seems from DOM insertion on react (out of my hand), followed by layouter pressure (also out of my hand) and negligible are sorting and rasterization of rows (that, too, is out of my hand)
- I think there can be done plenty on css hints about how the layout it supposed to do, but
containon rows sadly breaks subgrid positioning. - Performance can even now degrade. If you scroll plenty here, you will eventually hit JS/Chrome garbage collection, causing some stuttering, we've concluded from user tests that this is more a development-specific problem, nobody wildly scrolls up and down a lot
-
overscancan be increased to reduce the amount of "appearing" rows, that's mostly a trade-off of how much you want to clog the browser layouting stage - the example above lacks the overwriting of the virtualizer re-render, because something triggers immediate rerendering and I can't bother to debug that right now just for a demo
- out production app similarly linearizes the rows first in memory and then displays like this, we have a similar structure to that.
- out layout is plenty more complicated, which increases the layouter and DOM insertion pressure, with dev tools on, I can't get it beyond 120fps, which is quite poor to be fair
- this thing is missing a sliding window of how many rows it inserts per frame max, I'll have to write some more sophisticated code detecting when a browser rolls below vsync.
- maybe keeping a hot cache of rows may slightly add to the improvements, at least skipping the parent node of rows. I'd wager that the performance impact of just that one node is minimal.
@mio-moto Wow, great work! Thanks for this detailed summary. Do you maybe have some ideas on how we can get some reliable numbers to test the performance and have something to compare?
Patching the onChange handler to not use flushSync actually improves my rendering performance by roughtly 2x:
Did you also test react 19 to see if it has a similarly big performance impact? We should definitely allow control over it.