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
| ScrollView | FlatList | |
|---|---|---|
| Rendering | All children, immediately | Only what is on screen, plus a buffer |
| Memory with 1,000 items | All 1,000 mounted | Roughly 10 to 12 mounted |
| Mount time | Grows with the list | Roughly constant |
| Built-in pull to refresh | No | Yes, via refreshing and onRefresh |
| Built-in pagination | No | Yes, via onEndReached |
| Separators, headers, footers | Hand-rolled | Built in |
| Right for | Short, fixed content | Any 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 situation | Use |
|---|---|
| A settings screen with a dozen rows | ScrollView |
| A form that scrolls | ScrollView |
| A feed, a chat, a product list | FlatList |
| Anything paginated from an API | FlatList |
| Grouped sections with headers | SectionList |
| Thousands of rows, still janky on FlatList | Consider 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.
| Prop | What it does | Set it when |
|---|---|---|
| keyExtractor | Tells FlatList how to identify a row | Always, unless your items already have an id field |
| getItemLayout | Removes the need to measure rows | Every row is the same height. This is the single biggest win |
| initialNumToRender | How many rows to render on first paint | The first screen is slow to appear |
| windowSize | How many screens of content to keep mounted | Scrolling shows blank space, or memory is too high |
| removeClippedSubviews | Detaches off-screen views on Android | Long 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.