Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

"VirtualizedList: You have a large list that is slow to update" warning even though all components are optimized

I've been having lots of trouble trying to avoid getting the "VirtualizedList: You have a large list that is slow to update" warning when using a <FlatList> component with React-Native.

I've already done loads of research and Google-Fu to try a solution, but none of the solutions helped, not even the solution on this GitHub issue.

Things to keep in mind:

  • I'm using shouldComponentUpdate() { return false; } in my List Item component, so they are not updating at all.
  • The component that renders the FlatList is already a PureComponent
  • I added console.logs in the render methods of my components and can confirm they don't re-render.
  • I'm not using anonymous functions, so the renderItem component is not being re-initialized on each call.

Important: The warning only seems to occur after switching to a separate tab using my BottomTabNavigator and then coming back and scrolling through my list; but this is confusing because when I do this, the FlatList screen component is not re-rendering and neither are the list items. Since the components aren't re-rendering when browsing to another tab, why would this error happen?

Here's my exact code:

App.tsx

const AppRoutes = [
  { name: "Home", Component: HomeScreen },
  { name: "Catalog", Component: CatalogScreen },
  { name: "Cart", Component: CartScreen }
];

export const StoreApp = () => {
  return (
    <NavigationContainer>
      <StatusBar barStyle="dark-content" />
      <Tabs.Navigator>
        {AppRoutes.map((route, index) => 
          <Tabs.Screen
            key={index}
            name={route.name}
            component={route.Component}
            options={{
              headerShown: (route.name !== "Home") ?? false,
              tabBarIcon: props => <TabIcon icon={route.name} {...props} />
            }}
          />
        )}
      </Tabs.Navigator>
    </NavigationContainer>
  );
};

CatalogScreen.tsx

import React from "react";
import { FlatList, SafeAreaView, Text, View, StyleSheet } from "react-native";
import { LoadingSpinnerOverlay } from "../components/LoadingSpinnerOverlay";
import { getAllProducts, ProductListData } from "../api/catalog";

class ProductItem extends React.Component<{ item: ProductListData }> {
  shouldComponentUpdate() {
    return false;
  }

  render() {
    return (
      <View>
        {console.log(`Rendered ${this.props.item.name}-${Math.random()}`)}
        <Text style={{height: 100}}>{this.props.item.name}</Text>
      </View>
    );
  }
}

export class CatalogScreen extends React.PureComponent {
  state = {
    productData: []
  };
  
  componentDidMount() {
    getAllProducts()
    .then(response => {
      this.setState({ productData: response.data });
    })
    .catch(err => {
      console.log(err);
    });
  }

  private renderItem = (props: any) => <ProductItem {...props} />;
  private keyExtractor = (product: any) => `${product.id}`;
  private listItemLayout = (data: any, index: number) => ({
    length: 100,
    offset: 100 * index,
    index
  });

  render() {
    const { productData } = this.state;
    console.log("CATALOG RENDERED");

    return (
      <SafeAreaView style={styles.pageView}>
        {!productData.length && <LoadingSpinnerOverlay text="Loading products..." />}
        <View style={{backgroundColor: "red", height: "50%"}}>
          <FlatList
            data={productData}
            removeClippedSubviews
            keyExtractor={this.keyExtractor}
            renderItem={this.renderItem}
            getItemLayout={this.listItemLayout}
          />
        </View>
      </SafeAreaView>
    );
  }
};

const styles = StyleSheet.create({
  pageView: {
    height: "100%",
    position: "relative",
  }
});

Since my components and lists are optimized and I'm still receiving the error, I'm starting to believe that this may be an actual issue with React Native - but if anyone can see what I'm doing wrong, or any workarounds, this would help greatly!

Additional Findings: I found that the warning no longer occurs if the CatalogScreen component is contained inside of a NativeStackNavigator with a single Screen. I believe this may indicate that this is a problem with the BottomTabNavigator module.

For example, the no warning no longer occurs if I make the following changes:

App.tsx

const AppRoutes = [
  { name: "Home", Component: HomeScreen },
  { name: "Catalog", Component: CatalogPage }, // Changed CatalogScreen to CatalogPage
  { name: "Cart", Component: CartScreen }
];

CatalogScreen.tsx

const Stack = createNativeStackNavigator();

export class CatalogPage extends React.PureComponent {
  render() {
    return (
      <Stack.Navigator>
        <Stack.Screen 
          name="CatalogStack" 
          options={{ headerShown: false }}
          component={CatalogScreen}
        />
      </Stack.Navigator>
    );
  }
}

With this workaround, I'm rendering the Stack Navigation component instead of the CatalogScreen component directly. This resolves the problem, but I don't understand why this would make a difference. Does React Native handle memory objects differently in Stack Navigation screens as opposed to BottomTabNavigator screens?

like image 286
Jordan Arldt Avatar asked Nov 22 '21 00:11

Jordan Arldt


People also ask

How do you handle a large list in React native?

Use simple components Try to avoid a lot of logic and nesting in your list items. If you are reusing this list item component a lot in your app, create a duplicate just for your big lists and make them with less logic as possible and less nested as possible.

What is the difference between PureComponent and component?

The difference between them is that React. Component doesn't implement shouldComponentUpdate() , but React. PureComponent implements it with a shallow prop and state comparison. If your React component's render() function renders the same result given the same props and state, you can use React.

What is Virtualizedlist?

Virtualization massively improves memory consumption and performance of large lists by maintaining a finite render window of active items and replacing all items outside of the render window with appropriately sized blank space.

What component should update?

The shouldComponentUpdate method allows us to exit the complex react update life cycle to avoid calling it again and again on every re-render. It only updates the component if the props passed to it changes.


Video Answer


1 Answers

I'm just going to take a random shot at an answer, but it's at best a guess.

The video you've linked in the comments to your question made it a lot more clear what's happening, something strange is going on there.

With normal lists like those, especially on mobile, you want to render and load only the items that's currently being displayed and visible, you don't want to keep the entire list of all possible items in memory and rendered all the time. That's why you'd use something like a callback to render the item, so that the list can invoke the callback as it's being scrolled and render only the items it needs to.

That being said here are a couple of random things I can think of:

  • Make sure that every single component, including and especially the item component, are pure components so they don't rerender. Make really sure that item component isn't rendering again.
  • Try to alter your props destructure so that you directly receive the props, i.e. ({prop1, prop2}) instead of props with ...props. If you destructure the props like that it will create a new object every time an item is loaded. This could potentially be one of the culprits causing your issue, if the flatlist is constantly invoking the callback and creating tons of new objects. This could also potentially be an issue for the product item, if it sees that it's received a new prop reference, which means it's a different object, then it will have to do a shallow comparison (even if it's a pure component), on hundreds of props. That could really slow it down. The result is that it won't actually rerender it but it will still do hundreds of shallow object comparisons which is a slow process. So fix the destructuring, I'd bet something like this is causing the performance issue.
  • Make sure that you don't double render the view, don't render the flatlist before the data is actually loaded, before the state has been set only return the fallback spinner or loader and once the state has been set then return the full child. Otherwise you're double rendering the whole thing, even if there's no data.
  • Try and see if it makes a difference if you use an anonymous function as the renderItem callback, instead of using react class functions. I don't think it should make a difference but it's worth a shot if nothing else helps, since I'm not sure how the renderItem is utilizing this.

If all else fails I'd suggest you ask react native on github and in the meantime try using an alternative if there is one. On the other hand I don't really see any performance issues from the video, so perhaps it's also an error that can be safely ignored.

like image 181
Matriarx Avatar answered Sep 19 '22 22:09

Matriarx