Diagnose & Fix Janky FlatList Rendering in React Native
A step‑by‑step diagnostic guide to identify and fix janky FlatList rendering in React Native. Learn how to spot symptoms, audit keyExtractor and renderItem, tune windowing props, add getItemLayout, and when to move to FlashList.
09 Sept 2026, 08:57 UTC

Recognizable Symptoms
When you scroll a FlatList you may notice:
- Sudden frame‑rate drops (30 fps → 10 fps)
- Blank or flickering rows during fast flings
- Long initial paint when the list has >50 items, especially with images
- JS thread blocked for >16 ms on every scroll tick
Common Causes & Diagnostic Map
| Symptom | Likely Cause |
|---|---|
| Stutter on scroll | Inline renderItem recreates component each render |
| Blank cells in fast flings | Unstable keyExtractor (e.g., array index) |
| Slow first paint | Large initialNumToRender or windowSize |
| Inconsistent scrollToIndex | Missing getItemLayout for fixed‑height rows |
| Re‑render on parent state change | Rows receive new object/array props each render |
Ordered Diagnostic Checklist
- Confirm the list is the bottleneck
- Open the dev menu → Perf Monitor and record JS/UI frame rates while scrolling.
- If frame drops are concentrated in the
FlatListarea, proceed.
- Audit
renderItemdefinition- Is it declared inline inside
renderorreturn? - Does it create new functions or objects per row?
- Move it outside the render method or wrap the row component with
React.memo.
- Is it declared inline inside
- Verify
keyExtractorstability- Return a unique, immutable value (e.g.,
item.id). - Avoid index‑based keys when rows can reorder or prepend.
- Return a unique, immutable value (e.g.,
- Check parent state updates
- Does a state change in a parent component cause the entire list to re‑render?
- Use
React.memooruseMemoon the list wrapper to prevent unnecessary re‑renders.
- Measure windowing & clipping props
- Default
windowSizeis 21 viewports;maxToRenderPerBatchis 10. - For large lists, try
initialNumToRender={10},maxToRenderPerBatch={20},windowSize={7}. - Toggle
removeClippedSubviewsonly after confirming no disappearing content on Android.
- Default
- Implement
getItemLayoutif row height is fixed- Example:
const getItemLayout = (data, index) => ({ length: 80, offset: 80 * index, index, }); - Allows FlatList to skip measurement and speeds up scrollToIndex.
- Example:
Targeted Fixes for Each Finding
- Inline renderItem → Hoist to a separate memoized component:
const Row = React.memo(({item}) => { return {item.title}; }); // In render renderItem={({item}) => } - Unstable keys →
keyExtractor={item => item.id.toString()} - Large windowing defaults → adjust as shown in the checklist.
- Missing getItemLayout → add when rows are uniform height.
- Re‑renders on parent state change → wrap the
FlatListinReact.memoor move the list into a child component that only receivesdataandrefreshingprops.
Verification Steps
- After each tweak, re‑open Perf Monitor and scroll a fast‑fling; look for
JS frame dropsdropping below 10 ms per frame. - Insert a counter:
console.count('RowRender');inside the row component. The count should not increase on unrelated parent state updates. - Test on a low‑end physical Android device; simulator performance can be misleading.
- Run a quick benchmark:
react-native run-android --variant=releaseand use Android Profiler to confirm UI thread stays under 16 ms.
Escalation Criteria
- If memoization and windowing tweaks fail to bring frame rates above 60 fps during typical usage, proceed to profiling.
- Use Hermes profiler or Flipper to identify heavy JS work per scroll tick.
- Consider swapping to FlashList (a recycler‑style list) if the list contains deeply nested heterogeneous rows or large images.
- As a last resort, split the list into smaller chunks or lazy‑load heavy content outside of the list.
Practical Example
Below is a minimal FlatList that applies the above fixes:
import React, {useMemo} from 'react';
import {FlatList, Text, View} from 'react-native';
const Row = React.memo(({item}) => (
{item.title}
));
const MyList = ({data}) => {
const keyExtractor = useMemo(() => (item) => item.id.toString(), []);
const getItemLayout = useMemo(() => (data, index) => ({
length: 80,
offset: 80 * index,
index,
}), []);
return (
}
initialNumToRender={10}
maxToRenderPerBatch={20}
windowSize={7}
removeClippedSubviews={true}
getItemLayout={getItemLayout}
/>
);
};
Limitations & Caveats
- Prop defaults may differ between React Native 0.72 and 0.74; check your
react-nativeversion docs. - Enabling
removeClippedSubviewscan hide content that uses absolute positioning or complex transforms on Android. - FlashList is a third‑party library; evaluate its maintenance status before adding it to production.
- Memoization only helps if row props are referentially stable; avoid passing new object literals each render.
Conclusion
By systematically verifying the list as the bottleneck, stabilizing keys, hoisting render logic, tuning windowing, and, when necessary, providing getItemLayout, most jank in FlatList can be eliminated. Use the Perf Monitor and console counters to confirm each change addresses the real issue before escalating to deeper profiling or alternative list implementations.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.