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;
CodePudding user response:
Wrap your Flatlist item in memo, and function that changes checkbox state in useCallback, so only item that is changed is being rerendered and not the whole flatlist.
CodePudding user response:
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 updateTestScreen
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.