The Yoke app has an Instagram-like feed with video Reels and complex list-heavy screens across 100+ pages. Profiling with Flipper's React DevTools revealed FlatList components re-rendering 2–3× more than necessary, causing dropped frames, sluggish scroll, and a noticeable stutter when swiping between Reels. Here's the systematic fix.
Step 1: Identify re-render sources
Flipper's React DevTools highlighted three sources in order of severity: inline arrow functions in renderItem (creates a new function reference every render), plain object props like contentContainerStyle written inline (new object reference each render), and parent state changes — like a typing indicator — propagating down to unchanged list items.
Fix 1: Memoize renderItem and keyExtractor
// ❌ Before — new function reference every render
<FlatList
renderItem={({ item }) => <ReelCard item={item} onLike={handleLike} />}
keyExtractor={(item) => item.id.toString()}
/>
// ✅ After — stable references via useCallback
const renderItem = useCallback(
({ item }) => <ReelCard item={item} onLike={handleLike} />,
[handleLike] // handleLike also wrapped in useCallback
);
const keyExtractor = useCallback(
(item) => item.id.toString(), []
);
<FlatList renderItem={renderItem} keyExtractor={keyExtractor} />Fix 2: React.memo on list item components
Even with a stable renderItem, child components re-render if they don't implement shouldComponentUpdate or React.memo. Wrapping ReelCard in React.memo with a custom comparator that only checks id and likeCount (not the full item object) cut render count by half on its own.
const ReelCard = React.memo(
({ item, onLike }) => {
// component implementation
},
(prev, next) =>
prev.item.id === next.item.id &&
prev.item.likeCount === next.item.likeCount
);Fix 3: getItemLayout for fixed-height rows
For list screens with uniform item heights (notification list, followers list), getItemLayout tells FlatList the exact height without measuring. This eliminates the layout calculation pass and makes scrollToIndex instant. For Reels where each card is full screen height, this is trivial — CARD_HEIGHT = Dimensions.get('window').height.
const CARD_HEIGHT = Dimensions.get('window').height;
<FlatList
getItemLayout={(_, index) => ({
length: CARD_HEIGHT,
offset: CARD_HEIGHT * index,
index,
})}
windowSize={3} // render 1 above + 1 below viewport
maxToRenderPerBatch={2}
initialNumToRender={1}
/>Fix 4: Separate state that changes frequently
The typing indicator and unread badge count were stored in the same Redux slice as the message list. Any badge update triggered the entire ChatList to re-render. Moving ephemeral UI state (typing, badge) into a separate lightweight slice — and connecting only the header component to it — isolated re-renders to just the header.
After all fixes — memoised renderItem, React.memo with custom comparator, getItemLayout, windowSize tuning, and state isolation — Flipper showed a 40% reduction in re-renders. Average frame rate went from 45fps to a consistent 59–60fps on a mid-range Android test device.