Day 37: Mobile Performance: FlatList vs FlashList & Virtualization Tuning
Understand list virtualization deeply enough to correctly tune FlatList/FlashList props instead of cargo-culting them.
Study
Concepts
Virtualization: only render what is (nearly) on screen
Rendering 10,000 rows as real native views would exhaust memory and destroy scroll performance. Virtualization renders only the rows currently visible plus a small buffer ("window") above/below, recycling views as the user scrolls — rows that scroll far off-screen are unmounted (or recycled) rather than kept alive. FlatList (RN core) implements this via `VirtualizedList` underneath, tuned with props like `windowSize` (how many screens' worth of content to keep rendered around the viewport), `initialNumToRender`, `maxToRenderPerBatch`, and `removeClippedSubviews`.
The classic FlatList performance killer is `getItemLayout` being omitted for variable-height rows — without it, FlatList must measure each row after render to know scroll offsets, causing layout thrashing on fast scrolls; providing `getItemLayout` (when row height is known/fixed) lets it compute offsets mathematically instead, a significant win.
FlashList: recycling instead of mount/unmount
FlashList (Shopify) uses a fundamentally different technique — cell RECYCLING, similar to native `UICollectionView`/`RecyclerView`: instead of unmounting a row that scrolls off and mounting a brand-new one that scrolls in, it reuses the SAME underlying view instance and just updates its content, avoiding the (often expensive) mount/unmount lifecycle entirely on every scroll step. This is why FlashList asks for an `estimatedItemSize` up front — it needs a size estimate to plan recycling, whereas FlatList measures more reactively.
FlashList is close to a drop-in replacement for FlatList's API for most use cases, and is the current recommendation for RN lists with more than a few hundred items or with complex row components, where FlatList's mount/unmount churn becomes the visible bottleneck.
See It
Visualizations
Visualization
Virtualization window while scrolling a 10,000-row list
Only rows inside the window are mounted; the rest exist only as data.
Build It
Code Examples
A well-tuned FlatList for a fixed-height row
const ROW_HEIGHT = 72;
function ContactList({ contacts }) {
return (
<FlatList
data={contacts}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <ContactRow contact={item} />}
// Fixed row height -> compute offsets mathematically, no measuring pass
getItemLayout={(data, index) => ({
length: ROW_HEIGHT,
offset: ROW_HEIGHT * index,
index,
})}
initialNumToRender={12} // enough to fill the first screen
windowSize={7} // ~7 screens worth kept around the viewport
removeClippedSubviews // let Android reclaim off-screen native views
maxToRenderPerBatch={10}
/>
);
}The same list with FlashList — recycling by default
import { FlashList } from '@shopify/flash-list';
function ContactList({ contacts }) {
return (
<FlashList
data={contacts}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <ContactRow contact={item} />}
estimatedItemSize={72} // needed up front to plan cell recycling
/>
);
}Remember
Key Takeaways
- Virtualization mounts only visible-plus-buffer rows — the core technique behind every large-list solution.
- getItemLayout removes the need to measure rows for offset calculation — a major win when row height is known.
- windowSize/initialNumToRender/maxToRenderPerBatch trade memory and initial render cost against scroll smoothness — tune, don't guess.
- FlashList recycles view instances instead of mount/unmount per row, avoiding lifecycle churn on every scroll step.
- estimatedItemSize is FlashList's equivalent up-front hint to getItemLayout — required for its recycling strategy to plan correctly.
Do It
Practice
- 1Render a 5,000-item FlatList without getItemLayout, profile scroll performance, then add it and compare.
- 2Swap the same list to FlashList with a correct estimatedItemSize and compare JS frame drops using the RN Perf Monitor.
- 3Experiment with windowSize values (3 vs 21) on a heavy-row list and describe the memory-vs-smoothness tradeoff you observe.