virtual
virtual copied to clipboard
Scrolling up with dynamic heights stutters and jumps
Describe the bug
I have a feed of dynamic items, like iframes, photos, and text. When I scroll downward, everything works great and the scrolling is smooth, as measured items that increase in height push the other items down out of sight. However, when I scroll upwards, the performance is super stuttery and the items jump all over the place:
https://github.com/TanStack/virtual/assets/7350670/655b0a0b-4562-47e2-aa96-cb72daf8ad37
Your minimal, reproducible example
Code below:
Steps to reproduce
Create the virtualizer as normal:
const rowVirtualizer = useVirtualizer({
count: itemCount,
getScrollElement: () => parentRef.current,
estimateSize: () => 100,
overscan: 2
})
Then the feed:
const mainFeed = () => {
return (
<div
style={{
height: `${rowVirtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
const post = loadedPosts[virtualRow.index]
return (
<div
key={virtualRow.key}
data-index={virtualRow.index}
ref={rowVirtualizer.measureElement}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualRow.start - rowVirtualizer.options.scrollMargin}px)`,
display: 'flex',
}}
>
<div style={{ width: '100%' }}>
<FeedItem post={post} />
</div>
</div>
)
})}
</div>
)
}
Expected behavior
Scrolling upwards should be as smooth as scrolling downwards.
How often does this bug happen?
Every time
Screenshots or Videos
No response
Platform
macOS, Chrome
tanstack-virtual version
"@tanstack/react-virtual": "^3.0.1"
TypeScript version
No response
Additional context
The following code inside of the useVirtualizer
fixes the issue:
measureElement: (element, entry, instance) => {
const direction = instance.scrollDirection
if (direction === "forward" || direction === null) {
return element.scrollHeight
} else {
// don't remeasure if we are scrolling up
const indexKey = Number(element.getAttribute("data-index"))
let cacheMeasurement = instance.itemSizeCache.get(indexKey)
return cacheMeasurement
}
}
I propose that the above behavior is default, that items are not remeasured when scrolling upward.
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.
hmm that is interesting, the scroll backward is bit tricky, we are measuring and adjusting scroll position to remove this jumping, and it was working great for us.
Also keep in mind that when jumping to place in the list where we didn't scroll, when scrolling backward we only have estimated size that will break if we don't measure.
this happened to me too in Vue, my list only have 7 items and I already put my exact items height to estimateSize (776px). But in my case, not only the scroll up is stuttery, it loop back to previous item when I try to scroll up and create infinite loop of scrolling. It will look like the video below, in this video I try to scroll up but it loop me back to previous item
https://github.com/TanStack/virtual/assets/136794762/2d06b05d-d347-464f-ba89-a1656cc53b22
hmm that is interesting, the scroll backward is bit tricky, we are measuring and adjusting scroll position to remove this jumping, and it was working great for us.
Also keep in mind that when jumping to place in the list where we didn't scroll, when scrolling backward we only have estimated size that will break if we don't measure.
@piecyk what does the code look like to adjust the scroll position?
@nconfrey it's pretty simple, for elements above scroll offset we adjust scroll position with the difference
https://github.com/TanStack/virtual/blob/main/packages/virtual-core/src/index.ts#L662-L673
Experiencing this too but only in iOS Safari.react-virtuoso
suffers from this issue too.
Is there any solution to this? For example, would it be an option to skip the corrections?
Is there any solution to this?
Can you create an minimal reproducible example? It should work out of box, just testes dynamic example on safari and looks fine.
For example, would it be an option to skip the corrections?
Yes, for example passing your own elementScroll https://github.com/TanStack/virtual/blob/main/packages/virtual-core/src/index.ts#L207-L221 that will not add adjustments
Can you create an minimal reproducible example?
Ok, working on it.
Yes, for example passing your own elementScroll https://github.com/TanStack/virtual/blob/main/packages/virtual-core/src/index.ts#L207-L221 that will not add adjustments
Unfortunately, it still doesn't solve the problem. Will try to reproduce it in a sandbox.
I've got it kind of working with the following code. There are still rendering artefacts on mobile as elements are resized, but for our use case it's better than having no virtualisation. It needs a reasonably large overscan (i've got 20) to prevent empty space appearing at the top of the list on certain viewport widths. It's also necessary to kill the cache entirely. The better you can make your estimateSize
method, the fewer rendering artefacts you'll get.
export function VirtualInfiniteScroller<T>(props: VirtualInfiniteScrollerProps<T>) {
const {
rowData,
renderRow,
hasNextPage,
fetchNextPage,
isFetchingNextPage,
estimateRowHeight,
overscan,
} = props;
const listRef = useRef<HTMLDivElement | null>(null);
const estimateHeightWithLoading = (index: number) => {
if (index > rowData.length - 1) {
return LOADING_ROW_HEIGHT;
}
return estimateRowHeight(index);
};
const virtualizer = useWindowVirtualizer({
count: hasNextPage ? rowData.length + 1 : rowData.length,
estimateSize: estimateHeightWithLoading,
overscan: overscan ?? 20,
scrollMargin: listRef.current?.offsetTop ?? 0,
});
const virtualItems = virtualizer.getVirtualItems();
// Kill the cache entirely to prevent weird scrolling issues. This is a hack
virtualizer.measurementsCache = [];
useEffect(() => {
const [lastItem] = [...virtualItems].reverse();
if (!lastItem) {
return;
}
if (
hasNextPage &&
!isFetchingNextPage &&
lastItem.index >= rowData.length - 1
) {
fetchNextPage();
}
}, [
hasNextPage,
fetchNextPage,
rowData.length,
isFetchingNextPage,
virtualItems,
]);
return (
<div ref={listRef} className="List">
<div
style={{
height: virtualizer.getTotalSize(),
width: '100%',
position: 'relative',
}}
>
<div
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${
(virtualItems[0]?.start || 0) - virtualizer.options.scrollMargin
}px)`,
}}
>
{virtualItems.map((virtualItem) => {
return (
<div
key={virtualItem.key}
data-index={virtualItem.index}
ref={(el) => virtualizer.measureElement(el)}
>
{virtualItem.index > rowData.length - 1 ? (
<FlexSpinner />
) : (
renderRow(virtualItem.index)
)}
</div>
);
})}
</div>
</div>
</div>
);
}
I am encountering the same issue with react-virtuoso as you are. Is there any update on this problem?
I am encountering the same issue with react-virtuoso as you are. Is there any update on this problem?
hmm as mention above the size changes when scrolling up that causes the jumping because of scrolling element size change. We can't really fix this on library size as it's tightly coupled with specific implementation
Please use suggested workaround with custom measureElement
Maybe caching total size when scrolling backward also should limit the jumping, overall please share examples then i can have a look.
Has anyone managed to solve the issue of flickering when scrolling in reverse with react-virtuoso? I've tried using increaseViewportBy, overscan, and either itemSize or measureElement, but I'm still experiencing flickering/jumping while scrolling up.
Just ran into this issue with react-virtuoso. Are there any tips on how to fix that?
Just ran into this issue with react-virtuoso. Are there any tips on how to fix that?
It boils down to implementation of the item, why the sizes are changing are you loading some dynamic content. If you an share some stackblitz example maybe we can find a solution.
Ok, got the reproducible example https://stackblitz.com/edit/tanstack-query-dxabsn?file=src%2Fpages%2Findex.js The issue here is that the height of Item is async, in the example above simulating it via setTimeout.
NOTE: we are using vue version of this library.
Adding setTimeout() to :ref="measureElement" function solved the issue.
Note that virtualizer measureElement hack (described in The following code inside of the useVirtualizer fixes the issue:
) is NOT necessary.
const measureElement: VNodeRef = el => {
if (!(el && el instanceof Element)) return
setTimeout(() => { // this!
rowVirtualizer.value.measureElement(el)
})
}
The issue here is that the height of Item is async, in the example above simulating it via setTimeout.
I saw this and tried the workaround.
I added below option to useVirtualizer() for debug. Then I found ResizeObserverEntry comes even without any actual resize.
measureElement: (element, entry, instance) => {
const result = me(element, entry, instance)
console.log(entry ? 'has-entry' : 'not-has-entry', element.getAttribute('data-index'), result)
return result
},
Above log still reproduces even when we removed all of content so its height is zero. (Our row has dynamic content: filled combo box, automatic height textarea and some of filled input)
This does not change with setTimeout() hack, but maybe related?
Then I found ResizeObserverEntry comes even without any actual resize.
yep as the ResizeObserverEntry will also call for initial value
Has anyone solve this problem having dynamic item content? We fetch images from api and when user scrolls really fast down and up - list just jumps. I tried to add some image placeholder and keep the same height for image, but it doesn't help
@piecyk, might the big difference in the elements' size cause scrolling issues? I have 2 lists. On the first one elements' height varies from 250-290px and the scrolling is very smooth, without any issues. On the second list, the first element is over 1000px and the rest is around 250-350px. I set the estimate to 1100px and I can observe the scrolling issues, especially when scrolling through the first big element.
@kamil-homernik could be, if you can create something on stackblitz will have a look.
It was solved by adjusting the JSX structure and elements' sizes. In the end, we have 4 useWindowVirtualizer
instances with dynamic elements' height and it works perfectly. Thanks for this library.
faced with same problem, first time scroll perfectly, when back, scroll flickering ( in debug thousand messages about recalculate ranges getindexes etc and if no try to scroll , messages increase )
It was solved by adjusting the JSX structure and elements' sizes. In the end, we have 4
useWindowVirtualizer
instances with dynamic elements' height and it works perfectly. Thanks for this library.
can you share an example
@andrconstruction in our case there were two problems:
- Virtualizer was wrapped in memo (don't do that :smile: )
- As we were loading images from API, we have placeholders added, but there was bug in implementation: there were delay between placeholder unmount and image mount, so there were miliseconds where neither placeholder and image were mounted. It causes recalculation of height of whole item and then problem with the whole list
Try to optimise your JSX elements - in our case that was the problem, not virtualizer at all.
hope this helps someone in the future. Used object-fit: contain; height: [some value here] as CSS for img element, and this seems to have solved the issue for me. The suggested solution was giving me issues with scroll restoration.