React Native Masters-4: Optimizing FlatList Performance

ismail harmanda
Stackademic
Published in
9 min readJan 19, 2024

--

FlatList is a performant interface that we use to render large data sets in React Native projects in a performant and scrollable way.

Long story ☺️, grab your coffee ☕️

Terms

VirtualizedList: The component behind FlatList (React Native's implementation of the Virtual List concept.)

Memory consumption: How much information about your list is being stored in memory, which could lead to an app crash.

Responsiveness: Application ability to respond to interactions. Low responsiveness, for instance, is when you touch on a component and it waits a bit to respond, instead of responding immediately as expected.

Blank areas: When VirtualizedList can't render your items fast enough, you may enter a part of your list with non-rendered components that appear as blank space.

Viewport: The visible area of content that is rendered to pixels.

Window: The area in which items should be mounted, which is generally much larger than the viewport.

1. Use keyExtractor prop to give keys: Used to extract a unique key for a given item at the specified index. Key is used for caching and as the react key to track item re-ordering. The default extractor checks item.key, then item.id, and then falls back to using the index, like React does.keyExtractor tells the list to use the ids for the React keys instead of the default key property.

const renderItem = useCallback(({ item }) => <Item title={item.title} />, []);

<FlatList
data={listArray}
keyExtractor={(item, index) => item.id}
renderItem={renderItem}
/>;

2. Do not use anonymous inline functions: Since two different function instances are never equal, inline functions will cause the shallow equality comparison to fail, and thus will always trigger a re-render.

Bad practice:

import React from 'react';
import { View, FlatList, Text, TouchableOpacity } from 'react-native';

const InlineFunctionFlatList = () => {
const data = [
{ id: '1', text: 'Item 1' },
{ id: '2', text: 'Item 2' },
{ id: '3', text: 'Item 3' },
// ... more data
];

return (
<View>
<FlatList
data={data}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<TouchableOpacity onPress={() => alert(`Clicked ${item.text}`)}>
<Text>{item.text}</Text>
</TouchableOpacity>
)}
/>
</View>
);
};

export default InlineFunctionFlatList;

Good practice:

import React from 'react';
import { View, FlatList, Text, TouchableOpacity } from 'react-native';

const renderItem = (item, onPress) => (
<TouchableOpacity onPress={() => onPress(item.text)}>
<Text>{item.text}</Text>
</TouchableOpacity>
);

const NoInlineFunctionFlatList = () => {
const data = [
{ id: '1', text: 'Item 1' },
{ id: '2', text: 'Item 2' },
{ id: '3', text: 'Item 3' },
// ... more data
];

const handleItemClick = (text) => {
alert(`Clicked ${text}`);
};

return (
<View>
<FlatList
data={data}
keyExtractor={(item) => item.id}
renderItem={({ item }) => renderItem(item, handleItemClick)}
/>
</View>
);
};

export default NoInlineFunctionFlatList;

In the second example, we defined the renderItem function outside the FlatListcomponent. The handleItemClick function is also defined outside the FlatList. This is a better practice than writing inline functions in the renderItem prop.

Note: If you’re using a class-based component(older React and RN versions) then move the renderItem out of the render function.

Tip: If you’re using a functional component(highly recommended) then use useCallbackfor the renderItem function.

3. Avoid using too large Images: Sometimes(especially with low-end devices) Android can’t handle images with high resolution in the ListView (a native solution provided by Android to render a list of items). Even if we can reduce the resolution on the fly it will still cause some performance issues. So try to use sm/md resolution images. You can try to use max 720p but we must also have to keep it small enough. Otherwise, our images’ quality would be affected. Also then we can reduce the image fade duration to prevent image fade issues while scrolling quickly.

import React from "react";
import { View, FlatList, Text, TouchableOpacity } from "react-native";

const renderItem = (item, onPress) => (
<TouchableOpacity onPress={() => onPress(item.text)}>
<Text>{item.text}</Text>
</TouchableOpacity>
);

const List = () => {
const data = [
{ id: "1", text: "Item 1" },
{ id: "2", text: "Item 2" },
{ id: "3", text: "Item 3" },
// ... more data
];

const handleItemClick = (text) => {
alert(`Clicked ${text}`);
};

return (
<View>
<FlatList
data={data}
keyExtractor={(item) => item.id}
renderItem={({ item }) => renderItem(item, handleItemClick)}
fadeDuration={0}
/>
</View>
);
};

export default List;

4. Use initialNumToRenderprop to decide how many items to render in the initial batch. This should be enough to fill the screen but not much more. Note these items will never be unmounted as part of the windowed rendering to improve the perceived performance of scroll-to-top actions.

Our list component is getting better🚀:

import React from "react";
import { View, FlatList, Text, TouchableOpacity } from "react-native";

const renderItem = (item, onPress) => (
<TouchableOpacity onPress={() => onPress(item.text)}>
<Text>{item.text}</Text>
</TouchableOpacity>
);

const List = () => {
const data = [
{ id: "1", text: "Item 1" },
{ id: "2", text: "Item 2" },
{ id: "3", text: "Item 3" },
// ... more data
];

const handleItemClick = (text) => {
alert(`Clicked ${text}`);
};

return (
<View>
<FlatList
data={data}
keyExtractor={(item) => item.id}
renderItem={({ item }) => renderItem(item, handleItemClick)}
fadeDuration={0}
initialNumToRender={10}
/>
</View>
);
};

export default List;

5. Use getItemLayout prop:

(data, index) => {length: number, offset: number, index:number}

getItemLayout is an optional optimization that allows skipping the measurement of dynamic content if you know the size (height or width) of items ahead of time. getItemLayout is efficient if you have fixed-size items, for example:

import React from "react";
import { View, FlatList, Text, TouchableOpacity } from "react-native";

const renderItem = (item, onPress) => (
<TouchableOpacity onPress={() => onPress(item.text)}>
<Text>{item.text}</Text>
</TouchableOpacity>
);

const List = () => {
const data = [
{ id: "1", text: "Item 1" },
{ id: "2", text: "Item 2" },
{ id: "3", text: "Item 3" },
// ... more data
];

const handleItemClick = (text) => {
alert(`Clicked ${text}`);
};

const getItemLayout = (data, index) => ({
length: 50, // Assuming each item has a height of 50
offset: 50 * index,
index,
});

return (
<View>
<FlatList
data={data}
keyExtractor={(item) => item.id}
renderItem={({ item }) => renderItem(item, handleItemClick)}
fadeDuration={0}
initialNumToRender={10}
getItemLayout={getItemLayout}
/>
</View>
);
};

export default List;

6. removeClippedSubviewsmay improve scroll performance for large lists. On Android the default value is true. This prop removes off-screen views, freeing up resources. This may work like charming with large lists, but it might cause to delay when scrolling back to these unloaded views.

Note: May have bugs (missing content) in some circumstances — use at your own risk.*

import React from 'react';
import { View, FlatList, Text, TouchableOpacity } from 'react-native';

const renderItem = (item, onPress) => (
<TouchableOpacity onPress={() => onPress(item.text)}>
<Text>{item.text}</Text>
</TouchableOpacity>
);

const List = () => {
const data = [
{ id: '1', text: 'Item 1' },
{ id: '2', text: 'Item 2' },
{ id: '3', text: 'Item 3' },
// ... more data
];

const handleItemClick = (text) => {
alert(`Clicked ${text}`);
};

const getItemLayout = (data, index) => ({
length: 50, // Assuming each item has a height of 50
offset: 50 * index,
index,
});

return (
<View>
<FlatList
data={data}
keyExtractor={(item) => item.id}
renderItem={({ item }) => renderItem(item, handleItemClick)}
fadeDuration={0}
initialNumToRender={10}
getItemLayout={getItemLayout}
removeClippedSubviews={true}
/>
</View>
);
};

export default List;

7. Optimize large lists withmaxToRenderPerBatch to control the maximum number of items to render in each batch. The job of this prop is to limit the number of items rendered per batch. It is useful especially when dealing with large lists.

Pros: Setting a bigger number means fewer visual blank areas when scrolling (increases the fill rate).

Cons: More items per batch means longer periods of JavaScript execution potentially blocking other event processing, like presses, hurting responsiveness.

✨ How do I know the value I should give?: Although it is not universal and suitable for every project and situation, I follow this path in my projects. When the list items are rendered, I want there to be 2 to 3 more items at the bottom outside of the viewport. So, if there are 10 items in the viewport, I usually make maxToRenderPerBatch={13}, but if only 4 items fit, I make maxToRenderPerBatch={6}. Of course, you should decide this according to the character of the items listed and your needs.

✨And now our List component getting one more optimization:

import React from 'react';
import { View, FlatList, Text, TouchableOpacity } from 'react-native';

const renderItem = (item, onPress) => (✨
<TouchableOpacity onPress={() => onPress(item.text)}>
<Text>{item.text}</Text>
</TouchableOpacity>
);

const List = () => {
const data = [
{ id: '1', text: 'Item 1' },
{ id: '2', text: 'Item 2' },
{ id: '3', text: 'Item 3' },
// ... more data
];

const handleItemClick = (text) => {
alert(`Clicked ${text}`);
};

const getItemLayout = (data, index) => ({
length: 50, // Assuming each item has a height of 50
offset: 50 * index,
index,
});

return (
<View>
<FlatList
data={data}
keyExtractor={(item) => item.id}
renderItem={({ item }) => renderItem(item, handleItemClick)}
fadeDuration={0}
initialNumToRender={10}
getItemLayout={getItemLayout}
removeClippedSubviews={true}
maxToRenderPerBatch={13}
/>
</View>
);
};

export default List;

8. Smooth and efficient, windowSize:The number passed here is a measurement unit where 1 is equivalent to your viewport height. The default value is 21 (10 viewports above, 10 below, and one in between).

Pros: Bigger windowSize will result in less chance of seeing blank space while scrolling. On the other hand, smaller windowSize will result in fewer items mounted simultaneously, saving memory.

Cons: For a bigger windowSize, you will have more memory consumption. For a lower windowSize, you will have a bigger chance of seeing blank areas.

Let’s implement:

import React from 'react';
import { View, FlatList, Text, TouchableOpacity } from 'react-native';

const renderItem = (item, onPress) => (✨
<TouchableOpacity onPress={() => onPress(item.text)}>
<Text>{item.text}</Text>
</TouchableOpacity>
);

const List = () => {
const data = [
{ id: '1', text: 'Item 1' },
{ id: '2', text: 'Item 2' },
{ id: '3', text: 'Item 3' },
// ... more data
];

const handleItemClick = (text) => {
alert(`Clicked ${text}`);
};

const getItemLayout = (data, index) => ({
length: 50, // Assuming each item has a height of 50
offset: 50 * index,
index,
});

return (
<View>
<FlatList
data={data}
keyExtractor={(item) => item.id}
renderItem={({ item }) => renderItem(item, handleItemClick)}
fadeDuration={0}
initialNumToRender={10}
getItemLayout={getItemLayout}
removeClippedSubviews={true}
maxToRenderPerBatch={13}
windowSize={5}
/>
</View>
);
};

export default List;

9. Use memo(): React.memo() creates a memoized component that will be re-rendered only when the props passed to the component change. We can use this function to optimize the components in the FlatList.

import React, { memo, useCallback } from 'react';
import { View, FlatList, Text, TouchableOpacity } from 'react-native';

const renderItem = memo(({ item, onPress }) => (
<TouchableOpacity onPress={() => onPress(item.text)}>
<Text>{item.text}</Text>
</TouchableOpacity>
));

const List = () => {
const data = [
{ id: '1', text: 'Item 1' },
{ id: '2', text: 'Item 2' },
{ id: '3', text: 'Item 3' },
// ... more data
];

const handleItemClick = useCallback((text) => {
alert(`Clicked ${text}`);
}, []); // Empty dependency array because handleItemClick doesn't depend on any external variables

const getItemLayout = (data, index) => ({
length: 50, // Assuming each item has a height of 50
offset: 50 * index,
index,
});

return (
<View>
<FlatList
data={data}
keyExtractor={(item) => item.id}
renderItem={({ item }) => renderItem({ item, onPress: handleItemClick })}
fadeDuration={0}
initialNumToRender={10}
getItemLayout={getItemLayout}
removeClippedSubviews={true}
maxToRenderPerBatch={13}
windowSize={5}
/>
</View>
);
};

export default List;

We wrappedhandleItemClick function with useCallback with an empty dependency array. In this way, its reference remains constant unless the component is re-mounted. Also by wrapping renderItem with memo, the component will only re-render if its props (item and onPress) change. This refactor will improve performance by avoiding unnecessary re-renders of list items.

10. Use cached optimized images: You can use the community packages (such as react-native-fast-image from @DylanVann) for more performant images. Every image in your list is a new Image() instance. The faster it reaches the loaded hook, the faster your JavaScript thread will be free again.

While preparing this article, I received a lot of help from the RN documentation and even expressed some expressions directly as they were used in the documentation.

I hope you enjoyed. If you found this article insightful or helpful, consider supporting my work by buying me a coffee. Your contribution helps fuel more content like this. Click here to treat me to a virtual coffee ☕️. Happy hackings! 🚀

--

--

Sr. Mobile Developer / BSc- Systems and BSc- Software Eng. - React Native | iOS (Swift) You may support☕️: https://www.buymeacoffee.com/ismailharmanda