React Native

React Native FlatList Sluggish Scroll Performance With Large Data

Sluggish FlatList scrolling with large datasets almost always comes down to a combination of unnecessary re-renders of list items and suboptimal windowing configuration — FlatList is designed to handle large lists efficiently through virtualization, but several common implementation mistakes defeat that efficiency.

The Problem

A FlatList rendering a large number of items (hundreds or thousands) scrolls with visible stuttering, dropped frames, or a noticeable lag between the scroll gesture and the list actually moving, especially pronounced on lower-end Android devices.

Why It Happens

Several common patterns undermine FlatList's built-in virtualization and rendering optimizations:

  • The renderItem function is defined inline inside the component's render, creating a brand new function reference on every render, which defeats React's ability to skip re-rendering unchanged list items
  • List item components aren't memoized, so every re-render of the parent list causes every currently rendered item to also re-render, even if that specific item's own data hasn't changed at all
  • Missing or incorrect keyExtractor, causing React to lose track of which specific item is which across re-renders, forcing more complete re-renders than necessary
  • Expensive operations (image processing, complex calculations) happening directly inside each item's render, multiplied across however many items are currently rendered on screen
  • Default windowing settings not being tuned for the specific list — very large item sizes, or a very large total dataset, sometimes benefit from adjusting how many items are rendered outside the visible viewport

The Fix

Extract renderItem into a stable, memoized function using useCallback, rather than defining it inline on every render:

const renderItem = useCallback(({ item }) => (
  <ListItem data={item} />
), []);

<FlatList data={items} renderItem={renderItem} keyExtractor={(item) => item.id} />

Wrap the actual list item component itself in React.memo, so it only re-renders when its own specific props actually change, rather than re-rendering every visible item whenever the parent list re-renders for any reason:

const ListItem = React.memo(({ data }) => {
  return (
    <View>
      <Text>{data.title}</Text>
    </View>
  );
});

Always provide a correct, stable keyExtractor using a genuinely unique and stable identifier (a database ID, not an array index, since index-based keys become incorrect and misleading whenever the underlying list order or contents change):

<FlatList
  data={items}
  keyExtractor={(item) => item.id.toString()}
  renderItem={renderItem}
/>

Move expensive per-item computation out of the render path where possible — pre-compute derived values once when data is fetched or transformed, rather than recalculating them inside every item's render on every single render pass:

// Instead of computing inside renderItem on every render:
const formattedDate = formatExpensively(item.timestamp);

// Pre-compute once when data first arrives:
const itemsWithFormattedDates = rawItems.map(item => ({
  ...item,
  formattedDate: formatExpensively(item.timestamp),
}));

Tune FlatList's windowing props for your specific data and item sizes, rather than relying purely on defaults, particularly for lists with many items or unusually large/complex item layouts:

<FlatList
  data={items}
  renderItem={renderItem}
  keyExtractor={keyExtractor}
  windowSize={5}
  maxToRenderPerBatch={10}
  updateCellsBatchingPeriod={50}
  initialNumToRender={10}
  removeClippedSubviews={true}
/>

windowSize controls how many "screens" worth of content are rendered outside the visible viewport (both above and below); reducing it lowers memory and rendering overhead at the cost of potentially seeing brief blank space during very fast scrolling, while removeClippedSubviews (particularly beneficial on Android) unmounts views that scroll far enough outside the viewport rather than just visually hiding them.

For lists with items of genuinely fixed, known height, provide getItemLayout, which lets FlatList calculate scroll position and item placement without needing to actually measure each item's rendered size dynamically — this is one of the most impactful optimizations available for fixed-height lists specifically:

const getItemLayout = (data, index) => ({
  length: ITEM_HEIGHT,
  offset: ITEM_HEIGHT * index,
  index,
});

<FlatList data={items} getItemLayout={getItemLayout} ... />

Still Not Working?

If performance is still poor after applying these optimizations, and the dataset is genuinely very large (many thousands of items) or individual items are unavoidably complex to render, consider switching to FlashList from Shopify, a drop-in, largely API-compatible replacement for FlatList specifically engineered for significantly better performance with large datasets through a different underlying recycling strategy — for lists that remain sluggish even after all standard FlatList tuning, this is often a more effective path than continuing to micro-optimize within FlatList's own architecture.