React Native

FlatList Performance Best Practices for Long Lists in React Native

6 min read by DebuggedIt

Quick answer

A list that scrolls smoothly with fifty items and stutters with five thousand is one of the most common performance complaints in React Native apps. FlatList...

A list that scrolls smoothly with fifty items and stutters with five thousand is one of the most common performance complaints in React Native apps. FlatList is built to handle long lists, but it only performs well when your rows, props, and data flow cooperate with how it works. Most slowdowns come from a handful of repeatable mistakes rather than from the component itself.

How FlatList Keeps Long Lists Manageable

FlatList uses windowing: it renders only the rows near the visible area plus some buffer, and replaces the rest with blank space of the correct size. When you scroll fast, the list can briefly show blank areas because rows are not rendered yet. The goal of tuning is to make each row cheap to render and to avoid re-rendering rows that did not change.

That second point is where most apps lose frames. FlatList is a pure component that re-renders when its props change, and if you pass new function or object identities on every parent render, the list and its visible rows re-render each time. A state change in a parent, such as a text input or a counter, can end up re-rendering hundreds of rows if the list is not isolated from it.

Make Rows Cheap and Stable

Start with the row component. Wrap it in React.memo so it only re-renders when its own props change, and keep those props primitive where possible. Passing a whole mutable object that changes identity on every update defeats memoization. Pass an id and fetch derived data inside the row, or ensure unchanged items keep the same reference when you update the array.

Define renderItem and keyExtractor outside the render path, or memoize them with useCallback. An inline arrow function creates a new identity on each render, which makes FlatList treat the prop as changed. The key extractor should return a stable unique string, ideally the item's database id. Using the array index as a key causes React to reuse rows incorrectly when items are inserted or removed, which leads to wrong content and extra work.

const Row = React.memo(function Row({ id, title, onPress }) {
  return (
    <Pressable onPress={() => onPress(id)}>
      <Text>{title}</Text>
    </Pressable>
  );
});

const keyExtractor = (item) => item.id;

function Feed({ items }) {
  const handlePress = useCallback((id) => {
    // navigate or select
  }, []);

  const renderItem = useCallback(
    ({ item }) => (
      <Row id={item.id} title={item.title} onPress={handlePress} />
    ),
    [handlePress]
  );

  return (
    <FlatList
      data={items}
      renderItem={renderItem}
      keyExtractor={keyExtractor}
    />
  );
}

Keep rows light. Deeply nested layouts, heavy shadows, many absolutely positioned views, and large images all cost time during mount and layout. Resize images to the dimensions they are displayed at instead of loading full-resolution files, and avoid doing parsing, sorting, or formatting work inside the row's render function.

Tune the Windowing Props Carefully

FlatList exposes several props that control how much it renders. They are trade-offs, not free speedups, so change them one at a time and measure on a real mid-range device rather than a simulator.

  • initialNumToRender: how many rows render on first mount. Set it to roughly what fills the first screen. Too high slows the first render, too low causes a visible blank flash.
  • windowSize: the render window measured in viewport heights. A smaller value uses less memory and renders less, but increases the chance of blank areas during fast scrolls. A larger value does the opposite.
  • maxToRenderPerBatch and updateCellsBatchingPeriod: control how many rows are rendered per batch and how often. Larger batches fill the screen faster but can block the JavaScript thread longer.
  • removeClippedSubviews: detaches off-screen views from the native hierarchy. It can reduce memory, but it has been associated with rendering glitches in some cases, so test thoroughly before relying on it and check the documentation for your React Native version.
  • getItemLayout: if every row has a fixed height, providing this function lets FlatList skip measuring rows and makes scrollToIndex reliable. Do not use it when row heights vary, because wrong values cause misaligned scrolling.

Pitfalls to Avoid

Never place a FlatList inside a vertical ScrollView with the same scroll direction. The outer view renders all children at once, which disables windowing for the inner list and triggers warnings. Use ListHeaderComponent and ListFooterComponent to put extra content above or below the list instead.

Be careful with extraData. FlatList does not know when something outside data, such as a selected id, changed, so you pass it through extraData. Passing a new object each render forces updates every time, so pass the primitive value itself.

Finally, measure before optimizing. Use the React DevTools profiler to see which rows re-render and why, and the performance monitor to watch the JavaScript and UI frame rates. If a list with thousands of variably sized rows still struggles after these fixes, consider a list library built around view recycling, and evaluate it against your own data rather than relying on general benchmarks.

Frequently Asked Questions

Why do I see blank areas when I scroll fast?

FlatList renders rows asynchronously, so a fast scroll can outrun rendering. Reduce row cost first, then consider increasing windowSize or batch sizes, accepting higher memory use.

Should I use the index as a key?

Only for static lists that never reorder, insert, or delete. For anything dynamic, use a stable unique id to avoid incorrect row reuse and wasted renders.

Does React.memo always help?

Only when props are stable. If you pass a new inline object or function each time, memo compares and still re-renders, adding overhead without benefit.

Is pagination better than loading everything?

For large datasets, yes. Load pages with onEndReached and keep only what you need in memory, instead of putting tens of thousands of items in state.

Summary

Smooth long lists come from cheap, memoized rows, stable keys and callbacks, correct use of windowing props, and avoiding nested scrolling containers. Tune one setting at a time on real devices and verify with the profiler. For exact prop behavior and defaults, refer to the React Native documentation for the version your app uses.