necolas / react-native-web

Cross-platform React UI packages
https://necolas.github.io/react-native-web
MIT License
21.59k stars 1.78k forks source link

FlatList performance: Scrolling is buggy and not smooth when virtualization is enabled #1337

Closed brunolemos closed 4 years ago

brunolemos commented 5 years ago

The problem

It have a vertical FlatList with not that many items (~150). When virtualization is enabled and I scroll, there are lot's of FPS loss, I can see the lag clearly. Every time I scroll a little bit and a new dom element is added by the virtualization it happens.

On iOS simulator it's fast and smooth. On chrome is the worse, firefox is less bad but still not good.

If I use disableVirtualization, then it becomes super smooth, but it has its consequences, e.g. switching between screens is much more expensive.

I played with all the virtualization options, didn't help much.

Twitter is quite fast and also has dynamic height, does it use the same FlatList we use? If not, would love if they could open source it.

I've considered react-window but it doesn't seem to support dynamic height (just-in-time measurement) yet. https://github.com/bvaughn/react-window/issues/6. Maybe react-virtualized could help. The problem is that these external solutions don't support things like scrollToItem, onViewableItemsChanged, etc :(

GIF comparison

With virtualization enabled + getItemLayout forcing height:100

Slow and buggy

Kapture 2019-05-04 at 22 19 59

Without virtualization (disableVirtualization)

Scrolling is fast and smooth (screen change / first render is slower; memory usage is higher)

Kapture 2019-05-04 at 22 22 22

brunolemos commented 5 years ago

You can close this but I'd appreciate some insights.

sfcgeorge commented 5 years ago

I also noticed slow FlatList but I don't need dynamic heights so I was able to get good performance. Things to try:

I suspect getItemLayout will fix it for you. Before worrying about how to calculate the height, just set it to something fixed to test if that does solve the performance for you. If yes, then you can work out how to calculate.

I believe FlatList is taken straight from RN so I assume native Views + Yoga perform better than divs + flexbox in browser. Probably not much can be done without rewriting FlatList which I assume is out of scope as RNW tries to stay close to RN. A third party like Twitter could open source theirs (if they have one) but you'd have to ask them ¯⁠\_(ツ)_/⁠¯

brunolemos commented 5 years ago

PureComponent, keyExtractor

Thanks but I use that already.

getItemLayout

Just tried, unfortunately didn't help. Used getItemLayout to fix the height in 100. When scrolling, it still makes small jumps when the virtualization adds new items. The only thing that worked so far was disableVirtualization.

brunolemos commented 5 years ago

I've just edited the first comment with a gif comparison, check it out.

derekblank commented 5 years ago

@brunolemos I have experienced this when a <FlatList /> nested within a ScrollView contains many subviews (especially subviews containing images).

The example provided by RNW wraps FlatList with createAnimatedComponent from the Animated API, and also uses some other methods that may help you.

Also worth noting that while disableVirtualization is still active in RNW, it is deprecated in RN.

sfcgeorge commented 5 years ago

I think the answer is use React.PureComponent for all renderItem, ItemSeparatorComponent, ListHeaderComponent, etc. The example's item component uses PureComponent here: https://github.com/necolas/react-native-web/blob/0e81c6ef2758d4ca9b2099b1d04a4f8c417f0f43/packages/examples/src/RNTester/ListExampleShared.js#L45

It certainly seems to fix my code, for both FlatList and SegmentedList. A simpler example:

class FilterItem extends React.PureComponent {
  render() {
    const { item, index } = this.props
    return <Text key={index}>{item}</Text>
  }
}

class FilterSeparator extends React.PureComponent {
  render() {
    return <View />
  }
}

export default class SearchFilters extends React.Component {
  render() {
    return (
      <FlatList
        renderItem={({ item, i }) => <FilterItem item={item} index={i} />}
        ItemSeparatorComponent={FilterSeparator}
      />
    )
  }
}
brunolemos commented 5 years ago

I think it's some kind of bug, I already do all these common optimizations. Also there is no ScrollView above it. It works perfectly on ios and android, problem is only on browser.

Here's the source code: https://github.com/devhubapp/devhub/blob/f08c991f9d9ca324c23ebbf92e7a6a1de8428e22/packages/components/src/components/cards/NotificationCards.tsx#L330 You can try locally and remove all the disableVirtualization to see the issue.

0xpatrickdev commented 5 years ago

I am not entirely certain, but I think it might have something to do with FlatList and VirtualizedList not actually being implemented in RNW yet?

FlatList and VirtualizedList seem to be taken directly from react-native, and do not contain any of the DOM specific customizations like we see in ScrollView, TextInput, and View.

The vendor exports do, however, reference the DOM specific ScrollView and View components, so maybe that means they are actually implemented.

I think the FlatList performance suggestions in this thread are good, and definitely applicable since React is still managing the tree reconciliation, but may be outweighed by the need for a DOM specific optimization. I am not entirely sure what that optimization is, but I think @brunolemos is on to something with react-window.

As an aside, the React.PureComponent optimization really only matters when the component has functional props - i.e. an onPress handler or a navigate prop from react-navigation's withNavigation(). In the example above, FilterSeparator has no props, and FilterItem's props are both strings. In both instances, a shallowCompare would have the same effect as a normal comparison, making the need for a PureComponent unnecessary. The keyExtractor should also try and utilize a unique identifier for the item (i.e. item.id) instead of index, as this could cause unexpected behavior if the order of the items changes.

0xpatrickdev commented 5 years ago

@brunolemos have you given this a look? https://github.com/Flipkart/recyclerlistview

mayconmesquita commented 5 years ago

@brunolemos @pcooney10 i use https://github.com/Flipkart/recyclerlistview, the perfomance is so great!

jsp3536 commented 5 years ago

I noticed on iPhone 6s and Samsung Galaxy S8 (using Android 8) scrolling with flatlist is very smooth, but I tried two phones using Android 9 (Samsung Galaxy S10, Essential Phone) and scrolling was very choppy. I switched over to RecycleListView and now all phones scroll very smooth without jank.

brunolemos commented 5 years ago

@jsp3536 you talking about the web version running on the browser of these devices, right?

I may try RecycleListView soon but I'd really prefer to use react-window once it finishes https://github.com/bvaughn/react-window/issues/6.

RecycleListView seems to be missing a couple things I use but no big deal, e.g. https://github.com/Flipkart/recyclerlistview/issues/258. Also it has a lot of open issues and few activity.

jsp3536 commented 5 years ago

Yes. I am happy with the performance of RecycleListView but I need to checkout react-window

francescoagati commented 5 years ago

@brunolemos have you try with bounces false scrollEventThrottle 300 maxRenderToBatch 1 and updateCellsBatchingPeriod 100 and removeClippedSuvViews true (i dont know if this is implemented in web)

RichardLindhout commented 5 years ago

What I found is that every time there are extra items loaded the flatlist will re render the whole list. Even if you use purecomponent. I now use a normal scrollview on the web and the performance is much, much better!

francescoagati commented 5 years ago

What I found is that every time there are extra items loaded the flatlist will re render the whole list. Even if you use purecomponent. I now use a normal scrollview on the web and the performance is much, much better!

do you use keys?

francescoagati commented 5 years ago

and keys extractor?

RichardLindhout commented 5 years ago

@francescoagati Yes, you should try to make a flatlist and log the render function of the item with the index you will see that it will log every time extra items are added, instead of only the newly rendered ones

necolas commented 5 years ago

FlatList wasn't built to support the web, where the solution would probably have to involve juggling a bunch of web APIs to try to find extra performance from somewhere. I don't think supporting the web is a priority at the moment (cc @sahrens), but FB might eventually have a cross-platform solution and Google is working on a built-in virtualizer that might be something we can use one day.

brunolemos commented 5 years ago

I am not sure the problem is with FlatList.

I replaced it with react-window's FixedSizeList and I had pretty similar results.

Maybe there's something wrong with my app, maybe it's something on react-native-web internals, maybe it's react-window, maybe it's chrome, maybe it's the cost of the garbage collector. Needs more investigation, help much appreciated: https://github.com/devhubapp/devhub/issues/155

brunolemos commented 4 years ago

Closing since I don’t have these issues anymore and they might have been mostly because the renders were a bit expensive.

I’ve rewritten my list item components to be simpler. For example, their height are now pre-calculated (this is important) and they don’t do any computation inside the render. Also removed the deep nested custom components, it’s now a single ItemComponent as flat as possible.

Things are fast now.

I did switch to react-window on web, it is faster than FlatList but not a huge deal. The bigger optimizations were the other things I mentioned.

nihp commented 4 years ago

@brunolemos Any documentation related to disableVirtualisation for web, android and iOS

RichardLindhout commented 4 years ago

Just use a normal scrollview instead of a flatlist in the web @nihp.

nihp commented 4 years ago

@richardLindhout I am using iOS, here I need to know about the disableVirtualisation. I am using SrcollView and Flatlist in my app.

Need to smoothen the scroll in iOS so I asked about disableVirtualisation

RichardLindhout commented 4 years ago

There is no virtualization in the flatlist of react-native-web.

RichardLindhout commented 4 years ago

I'm wrong I think, it is available in the DevHub source: https://github.com/devhubapp/devhub/blob/f08c991f9d9ca324c23ebbf92e7a6a1de8428e22/packages/components/src/components/cards/NotificationCards.tsx#L339

nandorojo commented 3 years ago

I solved this in my app by remaking my renderItem component from scratch. It went from terrible, glitchy performance to butter. I was originally passing entire data objects to each list item. Then, in each component, I was checking my SWR cache to get more data about them.

For context, I was displaying an infinite scroll search list of artists (think Netflix search.) I wanted to show users if they've liked the artist before or not, etc. I was originally looping through a list, passing each list item down to my component, and then grabbing metadata about my list item from within each component. Why not? After all, each component could just access the global state, right? Turns out, this was very expensive.

I changed this by turning each card into a fully "dumb" component, with very little code. It only receives primitive props (hasLiked: boolean, starRating: number) and memoized functions (onPress(id: string)). This helped dramatically.

Oddly, adding getItemLayout caused it to flicker a lot on native. This is pretty surprising, given that I am using deterministic sizes. 🤷‍♂️

I highly recommend making your list item nimble. Don't pass the entire list object down to it – only the data it needs. Keep any functions at the top level of the list component, and memoize them before passing them down to each item. And, as people have mentioned here, memoize each list item if they re-render too often and you're seeing issues.

In case it's useful, these are the props I ended up with. I memoized every function I passed to FlatList (this might be overkill, but it worked for me.)

<FlatList
  data={hits}
  removeClippedSubviews={Platform.OS !== 'web'}
  renderItem={renderItemFast} 
  ListEmptyComponent={ListEmptyComponent} // memoized function
  numColumns={itemsPerRow}
  // force re-render when items per row changes, since this can't be changed on the fly
  key={`list-${itemsPerRow}`} 
  initialNumToRender={10}
  keyExtractor={keyExtractor}
  onEndReached={fetchMore}
  keyboardDismissMode="on-drag"
  keyboardShouldPersistTaps="handled"
  style={style.container} 
/>
viniciushernandes commented 2 years ago

Only add disableVirtualization and worked for me.

louicoder commented 2 years ago

Only add disableVirtualization and it worked for me.

This works like a charm. I'm curious to know where there's a deprecated label on it. I have tried to optimize my Flatlists in all ways but I would always get a warning Of Flatlist not optimized. This looks like the best option for now.

Also for those curious, there's another package called recyclerlistview which offers native performance

JalalMitali commented 1 year ago

@louicoder @viniciushernandes @brunolemos any better solution than disableVirtualization ? thanks in advance!

Lump01 commented 6 months ago

I'm creating a messaging feature to an app and was having issues with sorting then updating state with new messages (10 new ones at a time) as the user scrolls through old messages. I would face trouble with this as I got to 40-50 messages.

@nandorojo solution is good advice, but did not work for me because each message component has to calculate whether to show the date above it based on the previous message, pseudo-code is like this:

if (currentMessage.timestamp = today AND previousMessage.timestamp != today) {
  display "Today" above currentMessage;
}

(if current message was sent today AND previous message was not sent today, then show "Today" above current message and show yesterday above previous message)

Instead of sorting THEN updating the whole messages state variable in my FlatList, I resolved this by concatenating the new messages as the user scrolls and making the following change in my data prop on :

<FlatList
  removeClippedSubviews
  // disableVirtualization
  keyExtractor={keyExtractor}
  maxToRenderPerBatch={10}
  initialNumToRender={10}
  data={messages
        .slice()     // <------- add .slice() before .sort()
        .sort((a, b) => new Date(b?.createdAt) - new Date(a?.createdAt))}
  renderItem={renderItem}
  inverted
/>

Array.slice() does the following according to developer.mozilla.org:

The slice() method of Array instances returns a shallow copy of a portion of an array into a new array object selected from start to end (end not included) where start and end represent the index of items in that array. The original array will not be modified.

Updating 150 element long state array is very costly to performance especially when sorting WHILE scrolling. That was my issue. Another solution along the lines of what @nandorojo said is that you could do logic on the backend so it's easy to just display information based on what the front end receives.

Credit to this StackOverflow post and Mozilla's developer docs on this matter.

arys commented 5 months ago

increasing windowSize fixed it for me windowSize={40}