# React Native Performance: List Rendering

> Practical techniques for keeping large lists smooth in React Native — real-world experience with FlatList, memo, and keyExtractor.

- Published: 2024-08-04
- Category: Journal
- Tags: React Native
- Reading time: 3 min read
- Source: https://www.muhammetsafak.com.tr/en/blog/react-native-performance-list-rendering/
- Language: en-US
- Author: Muhammet Şafak

---
In Looplio, there was a noticeable slowdown when the user switched from the [calendar view](/en/blog/feeding-mobile-and-web-with-the-same-calendar-logic-looplio-diary/) to the full task list. The list was long; each item contained several components — a color label, date info, and completion status. Jank crept in during scrolling. The phone was powerful, but that didn't comfort me — I knew it would be worse on lower-end devices.

This became a React Native performance lesson.

## The difference between `ScrollView` and `FlatList`

The first mistake was using `ScrollView` for the list. `ScrollView` renders all child components at once. If the list has 50 items, all 50 are mounted and painted — even if the user can only see 10 of them.

`FlatList` (and its sibling `SectionList`) virtualizes rendering: only the items currently visible on screen, plus a small buffer, are rendered. As the user scrolls, items are recycled. For large lists, this is a critical difference.

```tsx
// Bad: renders all items at once
<ScrollView>
  {tasks.map((task) => (
    <TaskCard key={task.id} task={task} />
  ))}
</ScrollView>

// Good: renders only visible items
<FlatList
  data={tasks}
  keyExtractor={(item) => item.id}
  renderItem={({ item }) => <TaskCard task={item} />}
/>
```

## Why `keyExtractor` matters

Every list item in React needs a unique key so React can tell which items changed and which need to be re-rendered.

The `keyExtractor` prop tells `FlatList` how to derive that key. You should use a genuinely unique value like `item.id`; using `index` is tempting but leads to incorrect updates when items are added or removed from the list.

```tsx
<FlatList
  data={tasks}
  keyExtractor={(item) => item.id.toString()}
  renderItem={({ item }) => <TaskCard task={item} />}
/>
```

## Preventing unnecessary re-renders inside components

Even with virtualization, if the parent component re-renders on every scroll event, the `renderItem` function gets recreated — which causes every visible item in the list to re-render as well.

Two tools come into play here:

**`React.memo`**: Wraps the component and skips re-renders when props haven't changed.

```tsx
const TaskCard = React.memo(({ task }: { task: Task }) => {
  return (
    <View style={styles.card}>
      <Text style={[styles.title, task.completed && styles.completed]}>
        {task.title}
      </Text>
      <Text style={styles.date}>{formatDate(task.dueDate)}</Text>
    </View>
  );
});
```

**`useCallback`**: Memoizes the `renderItem` function so its reference stays stable even when the parent re-renders.

```tsx
const renderTask = useCallback(
  ({ item }: { item: Task }) => <TaskCard task={item} />,
  []
);

<FlatList
  data={tasks}
  keyExtractor={(item) => item.id.toString()}
  renderItem={renderTask}
/>;
```

The dependency array of `useCallback` matters. If `renderTask` reads any state from the parent component, you need to include it in the dependencies — otherwise you'll be closing over a stale value.

## Boosting scroll speed with `getItemLayout`

By default, `FlatList` measures each item's height after rendering it. If all your items have a fixed height, you can skip that measurement entirely:

```tsx
<FlatList
  data={tasks}
  keyExtractor={(item) => item.id.toString()}
  renderItem={renderTask}
  getItemLayout={(_, index) => ({
    length: TASK_CARD_HEIGHT,
    offset: TASK_CARD_HEIGHT * index,
    index,
  })}
/>
```

This makes a noticeable difference especially when using `scrollToIndex` — because FlatList already knows the exact position, it can jump there instantly.

## `windowSize` and `initialNumToRender`

Two important performance-related props on `FlatList`:

- `initialNumToRender`: how many items to render on the first pass. Default is 10; lowering it for tall cards reduces initial mount time.
- `windowSize`: the size of the render window. Default is 21 (10x the visible area above and below). Reducing it lowers memory usage, but fast scrolling may reveal blank areas.

Don't blindly tweak these values — test and tune them against your actual screen size and card content.

## Results in Looplio

After applying the changes — `React.memo`, `useCallback`, `getItemLayout` — scrolling through a 200-item list became smooth. React Native's Flipper integration and the JS thread frame rate monitor were useful for testing on a lower-powered device.

Performance optimization is not blind fixing. Measure first to find where the slowness actually is, then fix that. Applying `React.memo` to every component does not guarantee a performance gain; using it in the wrong place just adds unnecessary comparison overhead. Measuring beats guessing.
