You’re building a React Native feed for a large e-commerce marketplace (think flash sales + personalized recommendations) with 5–10M DAUs. A typical session scrolls through hundreds to thousands of items, each row containing images, price badges, and dynamic inventory status. On mid-tier Android devices, the team is seeing scroll jank, dropped frames, and occasional OOMs—directly impacting conversion during high-stakes events like Black Friday.
How would you optimize list rendering in a React Native application to keep scrolling smooth and memory stable?
In your answer, cover:
Assume you can change code in the screen and row components, but you can’t rewrite the app in a different framework. Discuss trade-offs (CPU vs memory, responsiveness vs prefetching) and call out common pitfalls (inline callbacks, dynamic row heights, image decoding). You may reference React Native primitives like FlatList/SectionList and general UI rendering principles, but focus on the underlying engineering reasoning rather than API trivia.
Virtualization renders only items near the viewport and reuses row views as you scroll, keeping memory and layout work bounded. The key is tuning how much off-screen content you keep (window size) to balance smoothness (prefetch) vs memory/CPU (too many mounted rows).
/* Conceptual: render only a window around visible indices */
visible = [firstVisibleIndex - overscan, lastVisibleIndex + overscan]
mount(items[i] for i in visible)
unmount(items outside visible)
Stable keys let React reconcile correctly and allow list virtualization to reuse cells safely. Using array indices as keys causes incorrect reuse when items insert/remove/reorder, leading to visual glitches, wrong state in rows, and extra mounts/unmounts.
// Good: stable ID
keyExtractor={(item) => item.id}
// Risky: index key
keyExtractor={(_, index) => String(index)}
Even with virtualization, if each scroll tick causes many rows to re-render, you’ll drop frames. Use React.memo (or equivalent), avoid creating new object/closure props per render, and ensure selectors/state updates are localized so only affected rows update.
const Row = React.memo(function Row({ item, onPress }) {
return <ItemCard item={item} onPress={onPress} />;
});
const onPress = useCallback((id) => navigate(id), [navigate]);
Smooth scrolling requires staying within a ~16ms/frame budget (or ~8ms on 120Hz). Defer non-critical work (analytics, heavy formatting), batch updates, and avoid expensive synchronous JS during scroll; otherwise the JS thread blocks and frames drop.
# Pseudocode: defer non-critical work
onScroll():
updateVisibleWindow()
scheduleIdle(() => precomputeBadgesForNextItems())
A non-virtualized list can behave like O(n) work/memory as n grows; virtualization aims to make work proportional to the window size w (O(w)) regardless of total items. The practical goal is to keep w small and keep per-row render cost low.
Total items: n
Mounted items with windowing: ~w (w << n)
Per scroll work target: O(w) not O(n)