Lesson 12 of 26
FlatList and Virtualization
What virtualization actually means for FlatList, why keyExtractor matters, and the performance props -- initialNumToRender, windowSize, getItemLayout, onEndReached.
You've already seen the basic shape of FlatList — data, renderItem,
keyExtractor — from the ScrollView vs. FlatList lesson, and why FlatList
wins the moment a list gets long. This lesson goes one level deeper: what
"virtualization" is actually doing under the hood, and the props that let
you tune it once the default behavior isn't enough.
What virtualization actually means
"Virtualization" sounds abstract, but the idea is concrete: at any given
moment, FlatList only keeps rendered (mounted) the items that are near the
visible viewport — the ones on screen, plus a small buffer above and below.
Everything else in your data array has no corresponding view at all right
now. As the user scrolls, items that scroll out of range get their views
recycled — reused for whatever new item is scrolling into range — rather
than every item in the array getting its own permanent, ever-growing pile
of native views.
- Step 1
User scrolls
The visible viewport shifts to a new range of the data array.
- Step 2
Items leave the viewport
Rows that scroll out of the visible-plus-buffer range are no longer needed on screen.
- Step 3
Views are recycled
Instead of being destroyed, their underlying native views are reused for incoming rows.
- Step 4
New items render
renderItem runs for the newly-visible data, filling the recycled views with the right content.
This is the entire reason FlatList can handle a list of ten thousand rows
about as cheaply as a list of ten: the cost is proportional to how many
rows are near the viewport, not how many rows exist in data.
Why keyExtractor is load-bearing, not cosmetic
You've used key in plain React lists already, and the reasoning here is
the same idea applied to something with real consequences. Because FlatList
is reusing views rather than creating a fresh one per item, it needs a
reliable way to know which logical item a given recycled view is now
supposed to represent. keyExtractor is that signal.
<FlatList
data={orders}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <OrderRow order={item} />}
/>Get this wrong — say, by keying on array index for a list that reorders or has items inserted in the middle — and you get the classic symptom: a row's visible content briefly doesn't match its data, or a component's internal state (like an expanded/collapsed toggle) ends up attached to the wrong row after a scroll. The fix is the same fix as on the web: key by something stable and unique to the item itself, like a database ID, not by position.
Tuning what gets rendered and when
The defaults are reasonable for most lists, but a few props exist for when they aren't:
-
initialNumToRender— how many items to render on first mount, before any scrolling happens. Lower it for a list whose first screenful is expensive to render; raise it if the default leaves visible blank space on a large device before FlatList catches up. -
windowSize— how large the rendered "window" around the viewport is, expressed as a multiple of the viewport's own height (both above and below). A larger window means smoother fast-scrolling at the cost of more memory and more work per render. -
getItemLayout— when every row has a fixed, known size, you can hand FlatList the exact size and position math yourself:<FlatList data={orders} keyExtractor={(item) => item.id} renderItem={({ item }) => <OrderRow order={item} />} getItemLayout={(_, index) => ({ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index, })} />Without it, FlatList has to measure items as they render to know where everything sits, which costs time and can cause visible jumpiness when jumping to a specific offset (like scrolling to an item by index).
getItemLayoutskips that measurement pass entirely, because you've already told it the answer — it only applies cleanly when rows really are a fixed, known size, which rules it out for content like chat messages with variable heights.
onEndReached: infinite scroll
Pagination is the other common tuning point. onEndReached fires once the
user has scrolled within a configurable distance (onEndReachedThreshold,
as a fraction of the viewport) of the bottom of currently loaded content:
function OrderList() {
const [orders, setOrders] = useState<Order[]>(initialOrders);
const [loadingMore, setLoadingMore] = useState(false);
const loadMore = async () => {
if (loadingMore) return;
setLoadingMore(true);
const nextPage = await fetchOrders({ after: orders.at(-1)?.id });
setOrders((prev) => [...prev, ...nextPage]);
setLoadingMore(false);
};
return (
<FlatList
data={orders}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <OrderRow order={item} />}
onEndReached={loadMore}
onEndReachedThreshold={0.5}
/>
);
}The loadingMore guard matters: without it, onEndReached can fire more
than once for the same approach to the bottom (it's a threshold crossing,
not a one-shot event), and an unguarded handler will happily fire off
several duplicate page-fetches before the first one even resolves.
What to remember
- Virtualization means only items near the viewport are mounted; scrolled-out views are recycled for incoming data rather than every item staying permanently rendered.
- A stable, unique
keyExtractoris what makes recycling correct — the same reasoning as React's listkey, with real bugs (stale content, misapplied state) if it's wrong. initialNumToRenderandwindowSizetune how much is rendered around the viewport, trading memory/work against scroll smoothness.getItemLayoutskips a measurement pass when every row's size is fixed and known up front — it doesn't apply to variable-height content.onEndReachedis the standard hook for infinite-scroll pagination; guard it against firing multiple overlapping fetches.
Check yourself
4 questions · pass 3/4 to unlock SectionList and Pull-to-Refresh
1.What does "virtualization" mean in the context of FlatList?
2.Why does a stable, unique keyExtractor matter for FlatList, for essentially the same reason React's key prop matters for a web list?
3.What does providing getItemLayout let FlatList skip?
4.What is onEndReached typically used for?
4 left to answer