# 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.

- Author: Syed Wajeeh Haider
- Published: Sep 28, 2026
- Category: React Native
- Tags: React Native, Performance, FlatList, FlashList
- Reading time: 3 min
- URL: https://syedwajeehhaider.com/fast-lists-react-native

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.

*Styles come from StyleSheet.create, not inline objects*

```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}
    />
  );
}
```

> **Watch the props:** `React.memo` only helps if props are stable. A `style={{ padding: 16 }}` literal or an inline `onPress={() => ...}` created in the parent defeats it, because every render passes a new object.

## 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.
