Flatlist represents a large list of data, it is very commonly used in mobile apps , yet it is very tricky, and performance can easily go down hell if it wasn’t taken into consideration since the beginning.
In this article, I will try to summarize how to analyze your Flatlist performance using Feed as a primary example, and what you can do to improve performance issues.
First step to having a performant Flatlist? Having a performant backend, yep, that’s it, that’s the article, just kidding , even with fast backend, your Flatlist can still have slow performance.
Flatlist comes with its own optimizations, but here are some steps to make it more performant.
Use Key Extractor
KeyExtractor tells the list to use the ids for the react keys instead of the default key property.
export const Feed = () => {
const feed = useFeed(); // Hook that fetches feed Data
return (
<FlatList
data={feed}
renderItem={({ item }) => <Post post={item} />}
keyExtractor={(_, index) => `post-${index}`}
/>
);
};
Avoid anonymous functions
Move out functions to the outside of render function or even outside the component(if they don’t depend on data from the component), so it won’t recreate itself each time render function called.
const renderItem = ({ item }) => <Post post={item} />;
const keyExtractor = (_, index) => `post-${index}`;
export const Feed = () => {
const feed = useFeed(); // Hook that fetches feed Data
return (
<FlatList data={feed} renderItem={renderItem} keyExtractor={keyExtractor} />
);
};
Get Item layout
If all your list item components have the same height, providing the getItemLayout prop eliminates the need for your FlatList to manage async layout calculations. Based on the React Native Docs, this is a very desirable optimization technique.
const renderItem = ({item}) => <Post post={item} />;
const keyExtractor = (_, index) => `post-${index}`;
const getItemLayout={(data, index) => (
{length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index}
)}
export const Feed = () => {
const feed = useFeed(); // Hook that fetches feed Data
return (
<FlatList
data={feed}
renderItem={renderItem}
keyExtractor={keyExtractor}
getItemLayout={getItemLayout}
/>
);
};
MaxToRenderPerBatch
This determines how many items are rendered per batch, which is the next chunk of items rendered on each scroll.
const renderItem = ({item}) => <Post post={item} />;
const keyExtractor = (_, index) => `post-${index}`;
const getItemLayout={(data, index) => (
{length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index}
)}
export const Feed = () => {
const feed = useFeed(); // Hook that fetches feed Data
return (
<FlatList
data={feed}
renderItem={renderItem}
keyExtractor={keyExtractor}
getItemLayout={getItemLayout}
maxToRenderPerBatch={6}
/>
);
};
However even with all these steps, you can still find yourself with slow feed if your render item is a component with deep nested components.
This is where the React profiler comes in handy. It helps identify the components that are causing performance issues.
Profiling
You can either use Remote Debugging or Flipper. I will be using Flipper since we are using Reanimated 2 in our project which doesn’t support Remote Debugger.
You open React Profiler, hit the record button, interact with the app a lil bit.
First, We will be taking a look at the Flame chart.
The bar at the top shows you the different commits that happened, and how long they took.
We click on the commit that took the longest time hence the commit with the highest bar.
Each bar in the chart represents a React component (e.g. View, Flatlist). The width of a bar represents how much time was spent when the component last rendered and the color represents how much time was spent as part of the current commit.
One of the components that seems to be causing an issue in our case is PostHeader. To have a more visual understanding of our problem, here is how the feed in our application looks like.
Optimize your images
One of the reasons this components is slow is due to images. To solve this problms here are some tips:
-
Use smaller-sized images (< 100kb).
-
Use react-native-fast-image to cache and optimize images.
useMemo & useCallback
If your component is performing some expensive calculations, make sure to wrap them with useMemo, useMemo will only recompute the memoized value when one of the dependencies has changed.
useCallback returns a memoized version of the callback that changes only if one of the dependencies changes. This is useful when passing callbacks to optimized child components that rely on reference equality to avoid renderings.
Memoize expensive component
In Flipper the bar in the right shows you how many times your component rerendered. For expensive components that you could not simplify any further, you can identify unnecessary rerenders, by analyzing your code or using tools like why-did-you-rerender.
Now you can memorize these components using React.memo,in our case you can wrap Post component with React.memo , however you can also avoid rerendering by bringing state up or down depending on your app structure, Dan Abramov have written a quite interesting blog post about it. You can check it out for more informations.
An important thing to remember, We’re currently measuring how well our app performs in development mode, which usually slower than the production mode due to all the development warnings, therefore I invite you to measure your app performance in production mode for more accurate results.
Recently, Shopify has announced a new fast and performant react native list, they had named it Flashlist. This new list recycles components under the hood to maximize performance, but more than that it relies on the Flatlist’s api, which means you can easily migrate to Flashlist and still get to use the tips and tricks we mentioned in this article.
As a conclusion, Performance was not something that I had to worry about in the earlier stage of our project, and you probably won’t too.
However always incorporate React Native best practices in your daily dev practices, and remember Make it work, Make it right, then Make it fast.