Skip to content
Muhammet Şafak
tr
Journal 3 min read

React Native Performance: List Rendering

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


In Looplio, there was a noticeable slowdown when the user switched from the calendar view 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.

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

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

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.

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:

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

Share:

Comments

Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.

Related Posts

Search the site

Start typing to search posts, projects and pages.

Esc to close Powered by Pagefind