Prevent the tail-spacer gets negative values in VirtualizedList on scrolling toward new items with dynamic height. #35413
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Summary
Flatlist on Web has scroll issues when used with expensive items.
I noticed this issue in an App called Expensify. It uses Flatlist for a chat of reports that supports text, images, emojis, and reports as items (and perhaps others that I am not aware of). These items have complex code to support interactions and features on them. As you scroll in the report chat in Web, especially if you scroll fast, you will notice scroll issues like scroll jumps, items appearing and disappearing, or items not showing at all.
Here's Expensify Flatlist Web current state:
Flatlist.Web.Broken.mp4
I recreate a sandbox with expensive items where you can experience the scroll issues I mentioned here.
Steps to reproduce the scroll issues in the sandbox:
I take on the task to improve the scroll experience of react-native-web's Flatlist. I found 3 problems that cause this issue and present their corresponding solution:
$lead_spacer expands scroll artificially when VirtualizedList is mounting new items —> Problem Explanation and Solution below.
VirtualizedList skip items for offset measuring when the user scrolls very fast while new items are mounted and measured —> Problem Explanation and Solution in this PR.
VirtualizedList gets offsets below or equal to zero for items that are not the list's first item —> Problem Explanation and Solution in this PR.
These solutions involve adding or modifying VirtualizedList.js but they improve drastically the scroll experience on Web without causing any problems on Android or iOS.
Also here's Expensify's App after solutions (plus another solution for Inverted VirtualizedLists in react-native-web):
Flatlist.Web.Good.mp4
This PR is the First Part solution to fix the 'Flatlist with expensive items breaks scroll' issue in react-native-web.
$lead_spacer expands scroll limit artificially when Inverted VirtualizedList is mounting new items. (1st of 3 problems that cause 'Flatlist with expensive items breaks scroll' )
In a FlatList with items with different heights, the scroll is limited to the last measured item. That’s because
VirtualizedList
(the innerFlatList
's component) must measure the offset of mounted items before it tries to mount new items. In this way, all items in the virtual area are measured and the_frames
object is complete.The scroll limit should be only expanded when the user tries to scroll beyond the scroll limit (unmeasured area) and then new items are mounted and their offsets are measured and saved in
_frames
. When recently mounted items are measured, the scroll limit is expanded to the new latest measured item, then the user will be ready to keep scrolling to unmeasured area and repeat the process to keep expanding the scroll limit until the last item is met.Sometimes users may “overscroll” to unmeasured areas while previously mounted items haven't been measured yet, and
VirtualizedList
starts skipping items for measuring and measures others that shouldn’t be measured yet, leading to a fragmented map of offsets (e.g. we have offset values for …31, 32, 33, then jumps to 37, 38…).Why do users “overscroll”?
Flaltlist constantly mounts and unmounts items. A white space is mounted on our list called
$tail_spacer
. It helps us to fill with white space the bottom outside of our virtual area (for the inverted list, it fills the top), where items were previously mounted and measured but unmounted afterward, so the scroll limit sticks to our latest measured item offset.$tail_spacer
height depends on how many areas we had measured, but it reduces to zero when the latest measured item is mounted and the user pretends to scroll to an unmeasured area looking for more items. When$tail_spacer
's height is zero, the scroll is limited and prevents the user from “overscroll”. This gives time to VirtualizedList to properly mount and measure new items to then use their offsets as our new scroll limit.Sometimes
$tail_spacer
has a height greater than zero when it should be zero, allowing the user to “overscroll”.Why sometimes does
$tail_spacer
has a height greater than zero when it should be zero?On some updates,
$tail_spacer
gets a negative value for its height, which is invalid on Web. Invalid values will be ignored and $tail-spacer’s height will set the valid value of the previous state, instead of setting it to zero.Solution:
The solution is simple. Set spacerSize to zero if the metrics calculation returns negative values.
Changelog
[GENERAL] [FIXED] prevent the tail-spacer gets negative values in VirtualizedList on scrolling toward new items with dynamic height.
Test Plan
This issue is difficult to reproduce on its own but fixing it does provide more stability used along with the other two solutions that I present with it. You can test the Flatlist with all solutions applied in this sandbox
Naturally, expensive items will take time to show but you should find no issues on scroll fast.
Also tested in Expensify's App I am working on (see video above).
No problem with iOS
iOS.Flatlist.Compressed.mp4
No problem with Android
Android.Flatlist.mp4
Thank you for reading, let me know what you think!