virtual icon indicating copy to clipboard operation
virtual copied to clipboard

Window & Dynamic Size List Virtualization Causes Mobile Safari IOS Scroll Momentum Interruption

Open kevin-sjj opened this issue 1 year ago • 1 comments

Describe the bug

By using the useWindowVirtualizer hook to apply virtualization for list items with different heights in relation to window scrolling, when you scroll, the virtualization behavior will occur.

Items that no longer need to be rendered will disappear from the DOM, and the list items corresponding to the visible indices will begin to appear in the DOM.

On Mobile iOS Safari, momentum scrolling triggered by user gestures works, but when combined with virtualization, the momentum effect fails to end smoothly and instead stutters or breaks abruptly. Is there a way to resolve this issue?

import "./styles.css";
import { useWindowVirtualizer } from "@tanstack/react-virtual";

const createRandomHeights = (
  min: number,
  max: number,
  count: number = 1000
) => {
  const heights = [];
  for (let i = 0; i < count; i++) {
    heights.push(Math.floor(Math.random() * (max - min + 1) + min));
  }
  return heights;
};

const randomHeights = createRandomHeights(100, 400);
const colors = ["red", "yellow", "blue"];

export default function App() {
  const virtualizer = useWindowVirtualizer({
    count: 1000,
    estimateSize: () => 400,
    overscan: 5,
    scrollMargin: 0,
  });

  const virtualItems = virtualizer.getVirtualItems();

  return (
    <div>
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          position: "relative",
          width: "100%",
        }}
      >
        <div
          style={{
            transform: `translateY(${
              (virtualItems[0]?.start ?? 0) - virtualizer.options.scrollMargin
            }px)`,
            position: "absolute",
            left: "0",
            top: "0",
            width: "100%",
          }}
        >
          {virtualItems.map((virtualItem) => {
            return (
              <div
                key={virtualItem.key}
                data-index={virtualItem.index}
                ref={virtualizer.measureElement}
                style={{
                  paddingTop: "40px",
                }}
              >
                <div
                  style={{
                    height: randomHeights[virtualItem.index],
                    backgroundColor: colors[virtualItem.index % 3],
                  }}
                ></div>
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
}

Your minimal, reproducible example

https://codesandbox.io/p/sandbox/qhkvyz

Steps to reproduce

  1. Go to CodeSandbox Link
  2. Scroll down Preview Page
  3. As you scroll, the momentum scroll stops occurring and the scrolling becomes increasingly choppy.

Expected behavior

I hope to maintain a seamless momentum scroll experience.

How often does this bug happen?

Every time

Screenshots or Videos

https://github.com/user-attachments/assets/2b27fcd6-e3de-449b-ac20-2633d76619d3

https://github.com/user-attachments/assets/166be2b0-cab1-495a-a9e1-9b770e31c1fc

Platform

  • OS: IOS
  • Browser: Safari
  • Version: 18.1, 17.6.1

tanstack-virtual version

v3.10.6

TypeScript version

v5.4.5

Additional context

It seems that the CodeSandbox link is difficult to run on Mobile iOS Safari at the moment. However, you can reproduce the issue by running the attached source code.

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.

kevin-sjj avatar Nov 22 '24 02:11 kevin-sjj

I'm no expert here, so take my findings with a pinch of salt

When an element is first "mounted" the size is measured, if the element is a different size from expected it will adjust the scroll position to hide this. Safari appears to only preserve a momentum scroll when doing this if it is the same direction, e.g. if currently scrolling down and I set the scroll to an earlier position, it will cancel it.

The result is that if scrolling down when seeing an element for the first time, the estimated size needs to be larger than elements actually are. This is called out in the docs - https://tanstack.com/virtual/latest/docs/api/virtualizer#estimatesize

If you are dynamically measuring your elements, it's recommended to estimate the largest possible size (width/height, within comfort) of your items. This will ensure features like smooth-scrolling will have a better chance at working correctly

The key for me was that I was implementing a message view which scrolls up by default, this flips the logic round and instead the estimate needs to be smaller than possible. Then when the user scrolls back down it will have already measured the elements and everything will work "correctly".

tustvold avatar Dec 13 '24 15:12 tustvold