ListFooterComponent prop raises performance warning if it's dynamic.
See original GitHub issueDescription
After few hours of debugging the FlatList/VirtualizedList performance warning:
VirtualizedList: You have a large list that is slow to update - make sure your renderItem function renders components that follow React performance best practices like PureComponent, shouldComponentUpdate, etc
I have found that ListFooterComponent
is causing the issue if we pass a dynamic value to it (e.g: ListFooterComponent={isLoading && <Footer />}
). If we pass a static value (e.g: ListFooterComponent={<Footer />}
, the warning will never appear.
Version
0.66.0
Output of npx react-native info
System: OS: macOS 11.6 CPU: Intel® Core i7 CPU @ 2.50GHz Memory: 16.00 GB Shell: 3.2.57 - /bin/bash Binaries: Node: 14.17.6 - ~/.nvm/versions/node/v14.17.6/bin/node Yarn: 1.22.11 - /usr/local/bin/yarn npm: 6.14.15 - ~/.nvm/versions/node/v14.17.6/bin/npm Watchman: 2021.09.06.00 - /usr/local/bin/watchman Managers: CocoaPods: 1.11.2 - /usr/local/bin/pod SDKs: iOS SDK: Platforms: DriverKit 21.0.1, iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0 IDEs: Xcode: 13.1/13A1030d - /usr/bin/xcodebuild npmPackages: @react-native-community/cli: Not Found react: 17.0.2 => 17.0.2 react-native: 0.66.0 => 0.66.0 react-native-macos: Not Found npmGlobalPackages: react-native: Not Found
Steps to reproduce
- Create a React Native app.
- Use the code example in the new React Native app.
- Build and launch the app.
- Keep scrolling until no more data can be loaded.
- Try to scroll up and the performance warning will appear right away, even that the list is not large (20 items).
Snack, code example, screenshot, or link to a repository
// App.tsx
import React from 'react';
import { SafeAreaView } from 'react-native';
import List from './List';
const App = () => {
return (
<SafeAreaView style={{ flex: 1 }}>
<List />
</SafeAreaView>
);
};
export default App;
// List.tsx
import React, { memo, useCallback, useEffect, useState } from 'react';
import {
ActivityIndicator,
ListRenderItem,
StyleSheet,
Text,
View,
VirtualizedList,
} from 'react-native';
const data = [
{ id: 1, name: 'Abarth' },
{ id: 2, name: 'Alfa Romeo' },
{ id: 3, name: 'Aston Martin' },
{ id: 4, name: 'Audi' },
{ id: 5, name: 'Audi Sport' },
{ id: 6, name: 'BAIC Motor' },
{ id: 7, name: 'BeiBen' },
{ id: 8, name: 'Bentley' },
{ id: 9, name: 'Berkeley' },
{ id: 10, name: 'BharatBenz' },
{ id: 11, name: 'Bizzarrini' },
{ id: 12, name: 'BMW' },
{ id: 13, name: 'Brabus' },
{ id: 14, name: 'Bugatti' },
{ id: 15, name: 'Cadillac' },
{ id: 16, name: 'Chevrolet' },
{ id: 17, name: 'Chrysler' },
{ id: 18, name: 'Corre La Licorne' },
{ id: 19, name: 'Dacia' },
{ id: 20, name: 'Daewoo' },
];
const getData = (from: number) => {
if (from > data.length) {
return [];
}
return data.filter((_, i) => i >= from && i < from + 5);
};
type ItemProps = {
name: string;
};
const ListItem = memo(({ name }: ItemProps) => {
return (
<View style={styles.itemContainer}>
<Text style={styles.itemText}>{name}</Text>
</View>
);
});
const Footer = () => {
return (
<View style={styles.footerContainer}>
<ActivityIndicator size={'large'} />
</View>
);
};
const List = () => {
const [isLoading, setIsLoading] = useState(false);
const [currentOffset, setCurrentOffset] = useState(0);
const [items, setItems] = useState<ItemProps[]>([]);
const getItemCount = useCallback((_data: ItemProps[]) => {
return _data.length;
}, []);
const getItem = useCallback((_data: ItemProps[], index: number) => {
return _data[index];
}, []);
const extractListKeys = useCallback(item => {
return `${item.id}`;
}, []);
const renderItem: ListRenderItem<ItemProps> = useCallback(({ item }) => {
return <ListItem name={item.name} />;
}, []);
const loadMoreData = useCallback(async () => {
if (isLoading) {
return;
}
setIsLoading(true);
await new Promise(resolve => setTimeout(resolve, 1500));
const _data = getData(currentOffset);
if (_data.length > 0) {
setItems(prevItems => [...prevItems, ..._data]);
}
setIsLoading(false);
setCurrentOffset(prevOffset => prevOffset + 5);
}, [currentOffset, isLoading]);
useEffect(() => {
loadMoreData();
// I don't want to add loadMoreData to the useEffect dependencies
// for the sake of this example.
// Adding it will create an infinite call to loadMoreData since it
// has dependencies (isLoading and currentOffset) that change a lot.
}, []);
return (
<View style={styles.screen}>
<View style={styles.topView}>
<Text style={styles.topViewText}>
{`currentOffset: ${currentOffset}, isLoading: ${
isLoading ? 'Yes' : 'No'
}`}
</Text>
</View>
<VirtualizedList
data={items}
initialNumToRender={4}
style={styles.list}
renderItem={renderItem}
getItem={getItem}
getItemCount={getItemCount}
keyExtractor={extractListKeys}
ListFooterComponent={isLoading && <Footer />}
onEndReached={loadMoreData}
/>
</View>
);
};
const styles = StyleSheet.create({
screen: {
flex: 1,
},
list: {
flex: 1,
},
itemContainer: {
height: 230,
justifyContent: 'center',
alignItems: 'center',
borderBottomWidth: 1,
},
itemText: {
fontSize: 20,
},
topView: {
padding: 14,
backgroundColor: '#999999',
},
topViewText: {
color: '#000000',
fontSize: 18,
},
footerContainer: {
alignItems: 'center',
padding: 20,
},
});
export default List;
Issue Analytics
- State:
- Created 2 years ago
- Reactions:1
- Comments:5
Top GitHub Comments
I am also facing the same issue with react-native version
0.64.3
.This issue was closed because it has been stalled for 7 days with no activity.