Skip to content

React Native FlatList vs ScrollView: What to Use and When

When to use FlatList and when ScrollView in React Native, the props that fix a slow list, and why nesting them is usually the wrong question.
6 min readMobile AppsReact Js

Introduction: FlatList vs ScrollView — A Common Developer Dilemma

If you’ve spent any time building mobile apps in React Native, you’ve probably used ScrollView and FlatList. And if you’re like most developers, you may have even used them interchangeably — especially for rendering vertical lists of content.

At first glance, they look similar. Both allow you to display scrollable views. But under the hood, they work very differently — and choosing the wrong one can lead to performance issues, memory overuse, and a poor user experience.

In this article, we’ll break down:

  • What FlatList and ScrollView actually are
  • When to use each one
  • Key differences in performance and features
  • Real-world examples and best practices

By the end, you’ll know exactly which to use — and why.

What Are ScrollView and FlatList?

Both ScrollView and FlatList are components provided by React Native to create scrollable content in your apps.

ScrollView

ScrollView renders all of its child components at once. That means if you pass it 1000 items, it will try to render all of them when the component mounts — regardless of whether they’re visible or not.

FlatList

FlatList is a more advanced component that is optimized for large data sets. It only renders the visible items on screen, and as the user scrolls, it lazily loads new content.

Key Differences Between FlatList and ScrollView

ScrollViewFlatList
RenderingAll children, immediatelyOnly what is on screen, plus a buffer
Memory with 1,000 itemsAll 1,000 mountedRoughly 10 to 12 mounted
Mount timeGrows with the listRoughly constant
Built-in pull to refreshNoYes, via refreshing and onRefresh
Built-in paginationNoYes, via onEndReached
Separators, headers, footersHand-rolledBuilt in
Right forShort, fixed contentAny list driven by data

ScrollView: When to Use It

You should use ScrollView when:

  • You’re rendering a small number of items (usually fewer than 20)
  • The content size is fixed or predictable
  • You need flexible layout control inside the scrollable area
  • Performance is not a concern
import { ScrollView, Text } from 'react-native';

<ScrollView>
  {items.map((item) => (
    <Text key={item.id}>{item.name}</Text>
  ))}
</ScrollView>

FlatList: When to Use It

You should use FlatList when:

  • You’re dealing with medium to large datasets
  • Your list contains repeating or structured items
  • You want to support features like: Lazy loading
  • Pull to refresh
  • Pagination
  • Item separators
import { FlatList, Text } from 'react-native';

<FlatList
  data={items}
  renderItem={({ item }) => <Text>{item.name}</Text>}
  keyExtractor={(item) => item.id.toString()}
/>

Common Mistakes to Avoid

This is the most frequent issue. Developers use ScrollView for a long list of items — which leads to performance problems like:

  • App crashes on lower-end devices
  • Slow rendering
  • Poor scroll experience
  • High memory consumption

If you have more than 20 list items, default to using FlatList.

Performance Comparison: Real-World Results

Let’s compare a list of 1000 items:

  • Loads all 1000 items in memory at once
  • Takes longer to mount the component
  • Scroll gets laggy or stuck on mid-range devices
  • Uses more CPU/GPU resources
  • Loads only 10–12 items (based on screen size)
  • Scroll stays smooth
  • Saves memory and boosts performance
  • Supports infinite scrolling and dynamic loading

Verdict: FlatList wins for performance every time you're dealing with unbounded or dynamic lists.

FlatList Features Worth Using

  • onEndReached: Trigger pagination when the user scrolls to the end
  • ListHeaderComponent & ListFooterComponent: Add headers or footers inside the list
  • refreshing & onRefresh: Add pull-to-refresh functionality
  • ItemSeparatorComponent: Automatically insert dividers between items
  • initialNumToRender: Control how many items render initially

When You Might Combine ScrollView and FlatList

In rare cases, you may need to nest scrollable components.

For example:

  • A parent ScrollView for vertical scroll
  • A child FlatList for horizontal scrolling (carousel)

But be careful! Nested scrolls can lead to gesture conflicts, especially on Android.

Summary: When to Use Which?

Your situationUse
A settings screen with a dozen rowsScrollView
A form that scrollsScrollView
A feed, a chat, a product listFlatList
Anything paginated from an APIFlatList
Grouped sections with headersSectionList
Thousands of rows, still janky on FlatListConsider FlashList

The FlatList props that actually matter

FlatList is faster than ScrollView out of the box, but most of the complaints about it being slow come down to four props left at their defaults on a list that needed them set.

PropWhat it doesSet it when
keyExtractorTells FlatList how to identify a rowAlways, unless your items already have an id field
getItemLayoutRemoves the need to measure rowsEvery row is the same height. This is the single biggest win
initialNumToRenderHow many rows to render on first paintThe first screen is slow to appear
windowSizeHow many screens of content to keep mountedScrolling shows blank space, or memory is too high
removeClippedSubviewsDetaches off-screen views on AndroidLong lists on Android, and test it, it can cause blanking

If you set only one of these, make it getItemLayout. Without it FlatList has to measure every row as it scrolls, which is exactly the work it exists to avoid.

<FlatList
  data={items}
  keyExtractor={(item) => item.id}
  getItemLayout={(_, index) => ({
    length: ROW_HEIGHT,
    offset: ROW_HEIGHT * index,
    index,
  })}
  initialNumToRender={10}
  windowSize={5}
  renderItem={({ item }) => <Row item={item} />}
/>

Stop re-creating renderItem on every render

A common and expensive mistake: defining renderItem inline means a new function on every render, so FlatList cannot tell that nothing changed and re-renders rows it did not need to.

// Every render creates a new function.
<FlatList renderItem={({ item }) => <Row item={item} />} />

// Stable across renders.
const renderItem = useCallback(({ item }) => <Row item={item} />, []);
<FlatList renderItem={renderItem} />

Pair that with React.memo on the row component itself. The two together are usually worth more than any prop you can tune, because they stop the work rather than making it faster.

What about SectionList?

SectionList is FlatList with grouping built in. If your data is naturally split into sections with headers, contacts grouped by letter, orders grouped by date, bookings grouped by month, use it rather than flattening your data and faking the headers inside a FlatList.

It takes the same performance props and behaves the same way. The only real difference is the shape of the data it expects.

When FlatList is still not fast enough

On very large lists, or rows with images and complex layouts, FlatList can still show blank space while scrolling fast. The usual next step is FlashList, Shopify's drop-in replacement, which recycles row views instead of mounting and unmounting them.

Two honest caveats before you reach for it. It is a third-party dependency, so it is one more thing to keep current. And most lists that feel slow on FlatList are slow because of the mistakes above rather than because of FlatList, so fix those first and measure again. Swapping libraries to avoid setting getItemLayout is not a fix.

Nested scrolling: usually the wrong question

Putting a FlatList inside a ScrollView produces a warning, gesture conflicts on Android, and the loss of the virtualisation you came for, because a FlatList with unbounded height renders everything.

The fix is almost always to invert it. Make the FlatList the outer component and put the content that was above it into ListHeaderComponent:

<FlatList
  data={items}
  ListHeaderComponent={<ProfileHeader />}
  ListFooterComponent={<Footer />}
  renderItem={renderItem}
/>

The header scrolls with the list, the list stays virtualised, and there is only one scroll gesture on screen. A horizontal FlatList inside a vertical one is fine, because the gestures do not compete.

Frequently asked questions

How many items before I should switch from ScrollView to FlatList?

There is no exact number, and twenty is a reasonable rule of thumb. The better question is whether the count is bounded. A settings screen has a fixed number of rows forever, so ScrollView is fine at any size. Anything driven by an API has no ceiling, so start with FlatList even while the list is short.

Why is my FlatList showing blank space while I scroll?

It is rendering more slowly than you are scrolling. Set getItemLayout if rows are a fixed height, raise windowSize, memoise the row component, and check whether each row is doing work it could do once, such as formatting dates or building objects inside the render.

Does FlatList work with pull to refresh and infinite scroll?

Both are built in. refreshing and onRefresh give you pull to refresh; onEndReached with onEndReachedThreshold gives you pagination. That is a genuine reason to choose it over ScrollView even for a short list that will grow.

Is ScrollView ever the better choice for performance?

Yes, for small fixed content. FlatList has real overhead: it measures, it tracks what is visible, it mounts and unmounts rows. On ten items that overhead is pure cost with no benefit. ScrollView is not a worse FlatList, it is a different tool.

Final Thoughts

Choosing between FlatList and ScrollView in React Native isn’t just about syntax — it’s about performance, user experience, and scalability.

Use ScrollView when the content is short and simple. Use FlatList when you're dealing with data — especially when the list might grow.

By making the right choice early, you avoid rework, crashes, and slow UI in production.

Share this
App work

Need the app actually built?

I build iOS and Android apps in React Native, with the back-end and the store submission included rather than left as somebody else's problem. A first conversation costs nothing.