FlatList
is slow when the data set is large (100s of items). Each click on an item's checkbox
takes around 2 seconds. Actually, the first click on a checkbox takes around a second; then, it increases by 20% till it reaches 2.5 seconds. It stays at 2.5 seconds for any clicks after that. It seems that the bottleneck is changing state
. Is there any way to improve the performance? Please, see a simplified code, which demonstrates the issue below.
const TestScreen = () => {
// Build a testing data set.
// In the actual application, the data set is stored in Redux.
let contactList = [];
for (let i = 0; i < 700; i++) { contactList.push({ mobileNo: i.toString() }); }
const [contactPhoneNumberList2, setContactPhoneNumberList2] = React.useState([]);
const _onPress = async (item) => {
// The following step takes no time because the number of selected items is very small!
const indexOfSelectedItem = contactPhoneNumberList2.findIndex(element => element === item.mobileNo);
// Get the current time BEFORE calling the state-changing step
const now = new Date();
// The user clicked on an already selected item -> let's unselect it (remove it from the list of selected items)
if (indexOfSelectedItem > -1) {
let abc = [ ...contactPhoneNumberList2 ];
abc.splice(indexOfSelectedItem, 1);
// Updating state takes around 2 seconds
// await is used below to help identify the bottleneck. It is NOT part of the production code.
await setContactPhoneNumberList2(abc); // <<< PROBLEM <<<
// Get the current time AFTER calling the state-changing step
const now2 = new Date();
console.log("state-changin took (milliseconds):", now2 - now, "onPress - IF - after state change", );
}
// The user clicked on an unselected item -> let's select it (add it to the list of selected items)
else {
// Updating state takes around 2 seconds
// await is used below to help identify the bottleneck. It is NOT part of the production code.
await setContactPhoneNumberList2(contactPhoneNumberList2.concat([item.mobileNo])); // <<< PROBLEM <<<
// Get the current time AFTER calling the state-changing step
const now2 = new Date();
console.log("state-changin took (milliseconds):", now2 - now, "onPress - IF - after state change", );
}
};
const _renderItem = ({ item, index, }) => {
// The following step takes no time because the number of selected items is very small!
const itemWasSelected = contactPhoneNumberList2.find(element => element === item.mobileNo);
return (
<ListItem>
<ListItem.CheckBox checked={itemWasSelected} onPress={() => _onPress(item)}/>
<ListItem.Content>
<ListItem.Title>{item.mobileNo}</ListItem.Title>
</ListItem.Content>
</ListItem>
);
};
return (
<FlatList
data={contactList}
// just in case, the natural key (item.mobileNo) is repeated, the index is appended to it
keyExtractor={(item, index) => item.mobileNo + index.toString()}
renderItem={_renderItem}
// I tried the following, but none of them seems to helpful, in this case
// removeClippedSubviews={false}
// initialNumToRender={5}
// maxToRenderPerBatch={10} // good
// windowSize={10}
// getItemLayout={_getItemLayout}
/>
);
};
export default TestScreen;
This problem isn't specific to FlatList or React Native. The issue is caused by your state change causing every item in the list to re-render on every change.
There are multiple ways to solve this:
The simplest solution would be to move state down to a deeper component, this would mean state updates are isolated to a single component, so you'll only have one render per state change.
This likely isn't practical because I assume you probably need access to your selected state in TestScreen
in order for it to be used for other purposes.
Memoize the component rendered in renderItem
. This will mean that with each state update TestScreen
will render, but then only the items that have changed will re-render.
Here's an example of how to achieve #2 and an Expo Snack example to play with:
import * as React from 'react';
import { Text, View, StyleSheet, FlatList, Button } from 'react-native';
import Constants from 'expo-constants';
const { useState, useCallback } = React;
const contactList = new Array(700)
.fill(0)
.map((_, index) => ({ mobileNo: index.toString() }));
export default function App() {
const [selected, setSelected] = useState([]);
const handlePress = useCallback((item) => {
// Using the state setter callback will give you access to the previous state, this has two advantages:
// 1. You don't need to pass anything to the dependency array of the `useCallback`, which means this
// function will remain stable between renders.
// 2. This will prevent any issues with rapid actions causing lost state because of stale data.
setSelected((previousState) => {
const index = previousState.indexOf(item.mobileNo);
if (index !== -1) {
const cloned = [...previousState];
cloned.splice(index, 1);
return cloned;
} else {
return [...previousState, item.mobileNo];
}
});
}, []);
return (
<FlatList
data={contactList}
keyExtractor={(item, index) => item.mobileNo + index.toString()}
// Important: All props being passed to <Item> must be stable/memoized
renderItem={({ item }) => (
<Item
// Only the `checked` property will change for the changed items
checked={selected.includes(item.mobileNo)}
onPress={handlePress}
item={item}
/>
)}
/>
);
}
// Memo will prevent each instance of this component from re-rendering if all of its props stay the same.
const Item = React.memo((props) => (
<ListItem>
<ListItem.CheckBox
checked={props.checked}
onPress={() => props.onPress(props.item)}
/>
<ListItem.Content>
<ListItem.Title>{props.item.mobileNo}</ListItem.Title>
</ListItem.Content>
</ListItem>
));
I also noticed in your example you were await
ing the state setter calls. This is not required, React state setters are not asynchronous functions.