Most React Native apps are lists: feeds, chats, orders, search results. When scrolling stutters or shows blank space, users notice immediately, and the cause is almost always the same handful of mistakes in how rows are rendered.
How FlatList renders
FlatList is a virtualized list. It renders a first batch of rows, then keeps a window of content around the viewport mounted and unmounts rows far outside it. The window is measured in screen heights: the default windowSize of 21 means ten screens above, ten below and the visible one. Anything that makes a single row expensive to render multiplies across that whole window.
Make rows cheap to re-render
The biggest wins come from stopping rows from re-rendering when nothing about them changed. That means a stable keyExtractor, a memoized row component, a renderItem that keeps the same identity between renders, and no fresh objects or arrow functions passed as props on every render.
const OrderRow = React.memo(function OrderRow({
order,
onPress,
}: {
order: Order;
onPress: (id: string) => void;
}) {
return (
<Pressable onPress={() => onPress(order.id)} style={styles.row}>
<Text style={styles.title}>{order.title}</Text>
<Text style={styles.meta}>{order.status}</Text>
</Pressable>
);
});
export function Orders({ orders }: { orders: Order[] }) {
const onPress = useCallback((id: string) => openOrder(id), []);
const renderItem = useCallback(
({ item }: { item: Order }) => <OrderRow order={item} onPress={onPress} />,
[onPress],
);
return (
<FlatList
data={orders}
keyExtractor={(o) => o.id}
renderItem={renderItem}
/>
);
}Tell the list how big rows are
If every row has the same height, getItemLayout lets FlatList skip measuring and jump straight to any offset. That makes scrollToIndex reliable and removes a layout pass per row.
const ROW_HEIGHT = 72;
<FlatList
data={orders}
keyExtractor={(o) => o.id}
renderItem={renderItem}
getItemLayout={(_, index) => ({
length: ROW_HEIGHT,
offset: ROW_HEIGHT * index,
index,
})}
initialNumToRender={12}
maxToRenderPerBatch={8}
windowSize={11}
/>- —
initialNumToRender: enough rows to fill the first screen, no more. - —
maxToRenderPerBatch: smaller batches keep the JS thread responsive while scrolling. - —
windowSize: lowering it saves memory and work at the cost of more blank space on fast flings. - —
removeClippedSubviews: detaches off-screen views and can help on long Android lists, but it has been known to cause missing content in some layouts, so test it rather than enabling it everywhere.
Images are usually the real cost
A row with a full-resolution photo decodes a large bitmap for a thumbnail-sized view. Serve images at the size they are displayed, give them explicit dimensions so layout does not shift, and use a caching image component so scrolling back up does not re-download everything.
When to reach for FlashList
Shopify's FlashList takes a different approach: instead of mounting and unmounting rows, it recycles row views, re-rendering an existing component with new data as it scrolls into view. For long, complex lists that means far less allocation and fewer blank frames. Earlier versions asked you to pass an estimatedItemSize; recent versions measure automatically and are built for the New Architecture, so check the version you install.
Because views are recycled, avoid keeping local state inside a row that belongs to a specific item, or it will appear on a different item after scrolling. For lists that mix row shapes, such as headers, messages and ads, provide getItemType so FlashList only recycles rows of the same kind into each other.
Measure, then change
Turn on the Perf Monitor from the dev menu to watch the JS and UI frame rates while you scroll, and use the React DevTools profiler to see which rows re-render and why. Always confirm in a release build on a mid-range Android device; development builds are much slower and flatter the problems on a fast phone.
Key takeaways
- —Stable keys,
React.memorows anduseCallbackrenderers stop needless re-renders. - —Never pass fresh inline objects or functions as row props.
- —Use
getItemLayoutwhenever row heights are fixed. - —Tune
initialNumToRender,maxToRenderPerBatchandwindowSizedeliberately. - —Size and cache images; they are often the real bottleneck.
- —Switch to FlashList for long, complex lists, and use
getItemTypefor mixed rows.
Smooth lists are not a single setting. They come from making each row cheap, telling the list what it needs to know, and measuring on the devices your users actually own.
