FlatList jank is the number one complaint I hear from React Native teams. The scroll stutters. Rows pop in. The first few seconds feel broken.
Most teams fix it wrong. They wrap every row in React.memo, crank initialNumToRender to 20, and ship a profile that's 5% better. Then they live with the jank.
The real cause is almost always one of three things: missing getItemLayout, unstable props in renderItem, or unoptimized images. Here's how I actually fix it.
The actual cause of scroll jank
FlatList measures every row by default. If a row is off-screen and FlatList doesn't know its height, it renders it invisibly to measure, then positions it. For a list of 500 items, that's 500 invisible renders.
This is the silent killer. The fix is getItemLayout — a function that tells FlatList the exact position and length of every row, no measurement needed.
<FlatList
data={items}
renderItem={renderItem}
getItemLayout={(data, index) => ({
length: ROW_HEIGHT,
offset: ROW_HEIGHT * index,
index,
})}
/>For fixed-height rows this is trivial and gives you instant scroll-to-index for free. For variable-height rows, you have to compute the cumulative offset yourself — which is annoying but worth it past 100 rows.
Fix #2: stop defeating React.memo
I see this in every codebase:
// Every render creates a new function reference
const renderItem = ({ item }) => (
<ProductRow
item={item}
onPress={(id) => navigateToProduct(id)}
style={{ backgroundColor: 'white' }}
/>
)Three problems:
onPressis a new function every renderstyleis a new object every render- Even if
ProductRowis wrapped inReact.memo, it re-renders every time
The fix:
// Hoist stable references
const ROW_STYLE = { backgroundColor: 'white' }
const handlePress = useCallback(
(id) => navigateToProduct(id),
[navigateToProduct]
)
const renderItem = useCallback(
({ item }) => (
<ProductRow item={item} onPress={handlePress} style={ROW_STYLE} />
),
[handlePress]
)
const ProductRow = memo(function ProductRow({ item, onPress, style }) {
// ...
})Now ProductRow only re-renders when its item actually changes. This is the single biggest FlatList win I've shipped — went from 30fps to 60fps on a client's product catalog.
Fix #3: images are probably your real bottleneck
Nine times out of ten when a team says "FlatList is slow," the actual bottleneck is image decoding on the JS thread.
The rules:
- Use a proper image library (
expo-image,react-native-fast-image) — not the built-inImage - Set
resizeModeexplicitly (default iscoverwhich can be expensive) - Pass explicit
sourcedimensions where possible - Cache aggressively — same URL should never decode twice
import { Image } from 'expo-image'
<Image
source={item.imageUrl}
style={styles.image}
contentFit="cover"
transition={150}
cachePolicy="memory-disk"
/>expo-image caches at the native level and decodes off the JS thread. The difference on a long list is night and day.
Fix #4: dial in the rendering budget
FlatList ships with conservative defaults that work for short lists and crush long ones.
<FlatList
data={items}
renderItem={renderItem}
maxToRenderPerBatch={4} // default 10 — too many for low-end devices
windowSize={8} // default 21 — keeps too many rows mounted
initialNumToRender={6} // what shows above the fold
removeClippedSubviews={Platform.OS === 'android'}
/>Lower maxToRenderPerBatch spreads rendering across more frames (smoother scroll, slightly slower initial load). Lower windowSize reduces memory.
Test on a real Android device, not the simulator. The simulator hides jank that real users feel.
Fix #5: when FlatList isn't enough
If you've done all of the above and the list is still janky on low-end devices, switch to FlashList from Shopify. It recycles cells at the native level — like UITableView on iOS — instead of mounting and unmounting React components.
import { FlashList } from '@shopify/flash-list'
<FlashList
data={items}
renderItem={renderItem}
estimatedItemSize={80}
/>estimatedItemSize is required and replaces getItemLayout. The API is close enough that most FlatList code ports in 10 minutes.
I've measured FlashList at 2-3x the FPS of a well-optimized FlatList on lists over 1,000 items. For short lists (under 50 items), the difference is negligible — don't add a dependency you don't need.
The workflow I use
- Profile on a real device with React DevTools Profiler
- Find the row component that's re-rendering most
- Stabilize its props (hoist styles, useCallback, useMemo)
- Add
getItemLayoutif rows are fixed-height - Switch to
expo-imageif the row renders images - Dial down
maxToRenderPerBatchandwindowSize - If still janky past 1,000 rows, migrate to FlashList
Steps 2 and 3 fix 70% of cases. Step 4 is the secret weapon most teams skip.
FlatList is not slow. Your renderItem is doing too much. Stabilize the props, add getItemLayout, and watch the jank disappear.
Want a React Native app that doesn't jank?
I build React Native apps that hit 60fps on real devices — not just on the simulator. Let's talk.