Kirito
The React Native FlatList That Re-rendered 1,000 Times Per Second
A FlatList with 2,000 items started re-rendering every frame, dropping to 5 FPS. The cause: an inline arrow function passed to renderItem.
The React Native FlatList That Re-rendered 1,000 Times Per Second
"Our app is so slow. Scrolling is janky."
Users were complaining about the product list screen. 2,000 items. Scrolling like a slideshow.
I opened the performance monitor: 5 FPS. CPU at 100%.
The Setup
A simple product list using Expo + React Native:
function ProductList({ products }) { const [selectedId, setSelectedId] = useState(null); const renderProduct = (item) => { return ( <ProductCard product={item} isSelected={item.id === selectedId} onPress={() => setSelectedId(item.id)} /> ); }; return ( <FlatList data={products} renderItem={({ item }) => renderProduct(item)} // ← PROBLEM keyExtractor={(item) => item.id} /> ); }
Looked innocent.
The Problem
Every time the user scrolled, the FlatList would re-render visible items.
But worse: every state change (like selecting a product) caused all 2,000 items to re-render, even the off-screen ones.
Why?
Inline arrow functions in renderItem create a new function on every render.
React Native's FlatList uses React.memo by default for its items. But when renderItem is a new function, the memo comparison fails. Every item re-renders.
With 2,000 items, each re-render triggered:
- 2,000 function calls
- 2,000 component instances re-evaluating
- JavaScript thread blocked for 200ms+
- UI drops frames
The Investigation
I added logs:
const renderProduct = useCallback((item) => { console.log('Rendering product', item.id); return <ProductCard ... />; }, [selectedId]); // Dependency on selectedId // Still not enough because renderItem is recreated
The log showed 2,000 "Rendering product" lines on every tap.
The Fixes
1. Extract renderItem outside component
// Outside component (stable reference) const renderProduct = ({ item, selectedId, onSelect }) => ( <ProductCard product={item} isSelected={item.id === selectedId} onPress={() => onSelect(item.id)} /> ); function ProductList({ products }) { const [selectedId, setSelectedId] = useState(null); // Use useCallback to memoize the renderItem function const renderItem = useCallback( ({ item }) => renderProduct({ item, selectedId, onSelect: setSelectedId }), [selectedId] ); return ( <FlatList data={products} renderItem={renderItem} keyExtractor={useCallback(item => item.id, [])} /> ); }
2. Use 'getItemLayout' for fixed height items
<FlatList getItemLayout={(data, index) => ({ length: 120, // Item height offset: 120 * index, index, })} // Prevents measurement of off-screen items />
3. Implement 'removeClippedSubviews'
<FlatList removeClippedSubviews={true} // Detach off-screen views windowSize={5} // Only render 5 screens worth of items maxToRenderPerBatch={10} updateCellsBatchingPeriod={50} />
4. Use 'React.memo' on ProductCard
const ProductCard = React.memo(({ product, isSelected, onPress }) => { // Component logic }, (prev, next) => { // Custom comparison return prev.product.id === next.product.id && prev.isSelected === next.isSelected; });
The Ultimate Solution
We refactored to use FlashList from Shopify (much more performant):
expo install @shopify/flash-list
import { FlashList } from '@shopify/flash-list'; function ProductList({ products }) { const [selectedId, setSelectedId] = useState(null); const renderItem = useCallback(({ item }) => { return ( <ProductCard product={item} isSelected={item.id === selectedId} onPress={() => setSelectedId(item.id)} /> ); }, [selectedId]); return ( <FlashList data={products} renderItem={renderItem} estimatedItemSize={120} keyExtractor={item => item.id} /> ); }
FlashList rendered at 60 FPS even with 10,000 items.
The Performance Metrics
Before:
- FPS: 5-10
- JS thread: 200ms per interaction
- Memory: 350MB
- Time to render initial list: 3.2s
After:
- FPS: 60
- JS thread: 16ms per interaction
- Memory: 120MB
- Time to render initial list: 0.4s
Commands to Debug FlatList Performance
Enable performance monitor
// In development import PerfMonitor from 'react-native-performance-monitor'; PerfMonitor.start();
Profile with Flipper
# Install Flipper # Then run your app with: npx react-native run-ios --configuration Release
Log render counts
const ProductCard = React.memo((props) => { const renderCount = useRef(0); renderCount.current++; console.log('ProductCard render count:', renderCount.current); return (...); });
What I Learned
- Never define 'renderItem' inline (always 'useCallback').
- FlatList re-renders all items when parent state changes unless memoized properly.
- 'getItemLayout' is essential for performance with many items.
- FlashList is superior for large lists – switch early.
That 5 FPS disaster taught our team to profile every list that could have more than 100 items. Now we have a rule: "Use FlashList for anything >50 items."