React Native Lists - Treat Them as a Performance Subsystem, Not a Component
10 Feb 2026In many React Native codebases, list performance is treated as a local concern; a screen feels slow, so someone tweaks FlatList props until itâs âgood enoughâ. initialNumToRender, windowSize, maybe getItemLayout if someone is feeling ambitious. Then the team moves on.
This approach works, until it doesnât.
Once your app grows, lists stop being just UI components. They become performance-critical infrastructure. Feeds, chats, search results, product catalogs, lists dominate runtime, memory usage, and perceived speed. At that point, tuning FlatList props is table stakes. The real gains come from treating lists as a subsystem with explicit rules, shared abstractions, and enforcement.
I decided to write this article to argue for that shift and shows what developers can do today to improve an existing React Native project.
The Ceiling of âJust Tune FlatListâ
Most experienced teams know the basics:
<FlatList
data={data}
keyExtractor={item => item.id}
initialNumToRender={10}
maxToRenderPerBatch={5}
windowSize={5}
removeClippedSubviews
/>
Sometimes they add:
getItemLayout={(_, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
This is good practice, but it assumes something rarely true in real apps:
- Items have stable, predictable layouts
- Data arrives all at once
- Every cell can render immediately
- All items follow the same composition rules
Once those assumptions break, FlatList tuning alone hits a ceiling. Scroll hitching returns. Memory spikes. Skeletons flash unpredictably. Developers start âfixingâ individual screens instead of addressing the root cause.
Thatâs the signal you need a system.
Lists as a Performance Subsystem
A subsystem has three properties:
- Explicit constraints
- Shared primitives
- Enforcement mechanisms
For lists, this means defining how items are measured, prefetched, rendered, and composedâand making those decisions reusable and reviewable.
Letâs break that down.
1. Item Measurement Is a First-Class Concern
Most teams rely on runtime measurement (onLayout) without realizing the cost. Measuring during scroll is expensive and unpredictable, especially with heterogeneous items.
An advanced approach is to design item measurement up front.
Strategy A: Fixed-height contracts
If an item can be fixed height, enforce it.
export const FEED_ITEM_HEIGHT = 96;
export function FeedItem({item}: Props) {
return (
<View style=>
{/* content */}
</View>
);
}
This unlocks cheap, deterministic getItemLayout and eliminates runtime measurement entirely.
Strategy B: Height classes
When fixed height isnât realistic, define height classes instead of infinite variability.
type ItemHeightClass = 'compact' | 'regular' | 'expanded';
const HEIGHTS: Record<ItemHeightClass, number> = {
compact: 64,
regular: 96,
expanded: 140,
};
Each item declares its class in data:
{
id: '123',
heightClass: 'regular',
}
Now getItemLayout is still computable, and layout variance stays bounded.
Actionable rule: No list item ships without an explicit height strategy.
2. Prefetching Is Part of Rendering, Not Networking
Many teams prefetch data globally and hope lists benefit indirectly. Thatâs backwards. Lists know exactly what the user will see next.
A list subsystem should own prefetching semantics.
Viewport-aware prefetch
const onViewableItemsChanged = useRef(({viewableItems}) => {
const lastVisible = viewableItems[viewableItems.length - 1];
if (!lastVisible) return;
const index = lastVisible.index ?? 0;
prefetchItems(index + 1, index + 10);
}).current;
<FlatList
onViewableItemsChanged={onViewableItemsChanged}
viewabilityConfig=
/>
This shifts prefetching from âwhen data loadsâ to âwhen the user scrollsâ.
Asset prefetching
Images, videos, and fonts should follow the same contract:
function prefetchItemAssets(item: FeedItem) {
Image.prefetch(item.thumbnailUrl);
}
Tie this into the same viewport logic.
Actionable rule: If an item canât render instantly, it must be prefetched before it enters the viewport.
3. Placeholder Strategy Must Be Deterministic
Skeletons are often implemented ad hoc: a shimmer here, a spinner there. The result is visual noise and layout shifts.
Instead, placeholders should be structurally identical to real cells.
Bad placeholder
- Different height
- Different layout
- Conditional branches everywhere
Good placeholder
function FeedItemPlaceholder() {
return (
<View style=>
<Skeleton width={48} height={48} />
<Skeleton width="60%" />
</View>
);
}
function FeedItemCell({ item }: Props) {
if (!item.isLoaded) {
return <FeedItemPlaceholder />;
}
return <FeedItem item={item} />;
}
The list doesnât care whether data is loaded. Layout stays stable. Scroll performance improves because React Native doesnât need to reflow items mid-scroll.
Actionable rule: Placeholders must match final layout dimensions exactly.
4. Cell Composition Rules Prevent Accidental Slowness
One of the most common performance regressions comes from âharmlessâ changes inside list items:
- Adding a new context provider
- Using anonymous inline functions
- Mounting a heavy hook per cell
- Conditional portals or modals
A list subsystem defines what is allowed inside a cell.
Cell boundary rule
Allowed
- Pure components
- Memoized callbacks
- Presentational hooks
Forbidden
- Navigation containers
- Global context providers
- Data-fetching hooks
Enforce this with structure:
function FeedItemCell(props: Props) {
return (
<CellBoundary>
<FeedItemView {...props} />
</CellBoundary>
);
}
const FeedItemView = React.memo(function FeedItemView(props: Props) {
// rendering only
});
Now performance assumptions are explicit and reviewable.
Actionable rule: Treat list cells like render() in a game engine: no side effects, no surprises.
5. Make the System Visible in Code Review
None of this matters if it lives in someoneâs head.
Strong teams encode list rules into the project itself:
- Shared
ListConfigobjects - Centralized
useListPerformance()hooks - ESLint rules for forbidden imports in /cells
- PR checklist items like:
- âDoes this change affect item height?â
- âAre placeholders layout-identical?â
- âIs this hook allowed in a cell?â
Example:
export const FeedListConfig = {
itemHeight: FEED_ITEM_HEIGHT,
prefetchAhead: 10,
placeholder: FeedItemPlaceholder,
};
Now list behavior is discoverable and consistent across screens.
Conclusion: Performance Is a Design Problem
FlatList tuning is necessary, but itâs not sufficient. Real-world React Native apps demand a more disciplined approach.
When you treat lists as a performance subsystem, you:
- Reduce regressions
- Make performance predictable
- Enable teams to move faster without fear
The key insight is simple but uncomfortable: most list performance issues are self-inflicted at the architecture level, not the prop level.
Design the system. Enforce the rules. Then let FlatList do what it does best.