FlatList mounts a fresh component tree for every cell that scrolls into view and unmounts it on the way out. Mount, unmount, mount again. On complex rows that churn is expensive enough to leave blank cells behind during a fast scroll, and to push memory up steadily as you page deeper into a long feed.
#The old way
import { FlatList, View, Text, StyleSheet } from 'react-native';
type Message = { id: string; sender: string; body: string };
function MessageList({ messages }: { messages: Message[] }) {
return (
<FlatList
data={messages}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.row}>
<Text style={styles.sender}>{item.sender}</Text>
<Text numberOfLines={2}>{item.body}</Text>
</View>
)}
/>
);
}
const styles = StyleSheet.create({
row: { padding: 12, borderBottomWidth: StyleSheet.hairlineWidth, borderColor: '#ddd' },
sender: { fontWeight: '600', marginBottom: 4 },
});
#The new way
import { View, Text, StyleSheet } from 'react-native';
import { FlashList } from '@shopify/flash-list';
type Message = { id: string; sender: string; body: string };
function MessageList({ messages }: { messages: Message[] }) {
return (
<FlashList
data={messages}
keyExtractor={(item) => item.id}
estimatedItemSize={72}
renderItem={({ item }) => (
<View style={styles.row}>
<Text style={styles.sender}>{item.sender}</Text>
<Text numberOfLines={2}>{item.body}</Text>
</View>
)}
/>
);
}
const styles = StyleSheet.create({
row: { padding: 12, borderBottomWidth: StyleSheet.hairlineWidth, borderColor: '#ddd' },
sender: { fontWeight: '600', marginBottom: 4 },
});
The props read the same. renderItem and keyExtractor keep the shape they had under FlatList. What changed sits underneath, where FlashList hands an existing cell instance the new data instead of tearing the tree down and building a replacement.
#Why it matters
Memory stays roughly constant however far down the list you scroll, because only the visible cells plus a buffer ever exist as mounted components. The blank-cell flash goes away too. A reused cell never passes through an unmounted state, so there is no empty frame left to render.
Shopify's own benchmarks put blank area and JS thread time markedly below FlatList on large lists.
#Gotcha
FlashList sizes its recycling pool from estimatedItemSize before the first cell renders. Skip the prop and it guesses. That rough internal guess is what produces the visible jump or thrash on first render, plus a scroll position that refuses to settle until the list has re-measured everything itself.
So set it. Anything close to your real row height will do.