All guides
React Native — GuideReact Native

Fast Lists in React Native: FlatList and FlashList

Why long lists stutter in React Native and how to fix them: FlatList tuning, memoized rows, fixed layouts, and when to switch to FlashList.

Syed Wajeeh Haider
Syed Wajeeh Haider
Full Stack Software Engineer
Sep 28, 20263 min read
#React Native#Performance#FlatList#FlashList

Most React Native apps are lists: feeds, chats, orders, search results. When scrolling stutters or shows blank space, users notice immediately, and the cause is almost always the same handful of mistakes in how rows are rendered.

10
default initialNumToRender for FlatList
10
default maxToRenderPerBatch
21
default windowSize, in viewport heights

How FlatList renders

FlatList is a virtualized list. It renders a first batch of rows, then keeps a window of content around the viewport mounted and unmounts rows far outside it. The window is measured in screen heights: the default windowSize of 21 means ten screens above, ten below and the visible one. Anything that makes a single row expensive to render multiplies across that whole window.

Make rows cheap to re-render

The biggest wins come from stopping rows from re-rendering when nothing about them changed. That means a stable keyExtractor, a memoized row component, a renderItem that keeps the same identity between renders, and no fresh objects or arrow functions passed as props on every render.

tsx
const OrderRow = React.memo(function OrderRow({
  order,
  onPress,
}: {
  order: Order;
  onPress: (id: string) => void;
}) {
  return (
    <Pressable onPress={() => onPress(order.id)} style={styles.row}>
      <Text style={styles.title}>{order.title}</Text>
      <Text style={styles.meta}>{order.status}</Text>
    </Pressable>
  );
});

export function Orders({ orders }: { orders: Order[] }) {
  const onPress = useCallback((id: string) => openOrder(id), []);
  const renderItem = useCallback(
    ({ item }: { item: Order }) => <OrderRow order={item} onPress={onPress} />,
    [onPress],
  );

  return (
    <FlatList
      data={orders}
      keyExtractor={(o) => o.id}
      renderItem={renderItem}
    />
  );
}
Styles come from StyleSheet.create, not inline objects

Tell the list how big rows are

If every row has the same height, getItemLayout lets FlatList skip measuring and jump straight to any offset. That makes scrollToIndex reliable and removes a layout pass per row.

tsx
const ROW_HEIGHT = 72;

<FlatList
  data={orders}
  keyExtractor={(o) => o.id}
  renderItem={renderItem}
  getItemLayout={(_, index) => ({
    length: ROW_HEIGHT,
    offset: ROW_HEIGHT * index,
    index,
  })}
  initialNumToRender={12}
  maxToRenderPerBatch={8}
  windowSize={11}
/>
  • —initialNumToRender: enough rows to fill the first screen, no more.
  • —maxToRenderPerBatch: smaller batches keep the JS thread responsive while scrolling.
  • —windowSize: lowering it saves memory and work at the cost of more blank space on fast flings.
  • —removeClippedSubviews: detaches off-screen views and can help on long Android lists, but it has been known to cause missing content in some layouts, so test it rather than enabling it everywhere.

Images are usually the real cost

A row with a full-resolution photo decodes a large bitmap for a thumbnail-sized view. Serve images at the size they are displayed, give them explicit dimensions so layout does not shift, and use a caching image component so scrolling back up does not re-download everything.

When to reach for FlashList

Shopify's FlashList takes a different approach: instead of mounting and unmounting rows, it recycles row views, re-rendering an existing component with new data as it scrolls into view. For long, complex lists that means far less allocation and fewer blank frames. Earlier versions asked you to pass an estimatedItemSize; recent versions measure automatically and are built for the New Architecture, so check the version you install.

Because views are recycled, avoid keeping local state inside a row that belongs to a specific item, or it will appear on a different item after scrolling. For lists that mix row shapes, such as headers, messages and ads, provide getItemType so FlashList only recycles rows of the same kind into each other.

Measure, then change

Turn on the Perf Monitor from the dev menu to watch the JS and UI frame rates while you scroll, and use the React DevTools profiler to see which rows re-render and why. Always confirm in a release build on a mid-range Android device; development builds are much slower and flatter the problems on a fast phone.

Key takeaways

  • —Stable keys, React.memo rows and useCallback renderers stop needless re-renders.
  • —Never pass fresh inline objects or functions as row props.
  • —Use getItemLayout whenever row heights are fixed.
  • —Tune initialNumToRender, maxToRenderPerBatch and windowSize deliberately.
  • —Size and cache images; they are often the real bottleneck.
  • —Switch to FlashList for long, complex lists, and use getItemType for mixed rows.

Smooth lists are not a single setting. They come from making each row cheap, telling the list what it needs to know, and measuring on the devices your users actually own.

Keep reading
(10)Let's talk

Ready to scaleyour application?

Stop worrying about technical debt. I can help you build a reliable, high-performance product from the ground up, tailored exactly to your business needs.

Actively open for new opportunities
Get in touch
Local time
--:-- PKT
© 2026 Syed Wajeeh Haider. All rights reserved.