Use FlatList for any list built from data, especially one that can grow, because it only renders the rows near the screen. Use ScrollView for short, fixed content such as a settings screen, a form or a detail page, because it renders every child at once and that is only cheap while there are few children.
That is the whole decision for most screens. The rest of this guide shows how each one works, the FlatList props that fix a slow list, the mistakes that cause most performance complaints, and where SectionList and FlashList fit. It is the same checklist I use on client apps as part of my React Native development work, and every prop below is checked against the official React Native documentation.
FlatList vs ScrollView at a glance
| ScrollView | FlatList | |
|---|---|---|
| Rendering | Every child, as soon as it mounts | Rows near the viewport, rendered in batches as you scroll |
| Memory | Grows with every item you add | Bounded by windowSize (21 screen lengths by default) |
| Large lists (hundreds of rows or more) | Slow to mount and heavy to keep | What it is built for |
| Headers and footers | Ordinary children above or below the content | ListHeaderComponent, ListFooterComponent and ListEmptyComponent |
| Pull to refresh | refreshControl with a RefreshControl (vertical only) | refreshing and onRefresh |
| Sticky headers | stickyHeaderIndices | stickyHeaderIndices, or SectionList for sticky section headers |
| Infinite scroll | Hand-rolled from scroll events | onEndReached and onEndReachedThreshold |
| Typical use | Settings, forms, profile and detail screens | Feeds, chats, search results, anything from an API |
How ScrollView works, and when to use it
ScrollView is a scrollable container. The React Native docs are direct about the trade-off: it renders all of its children at once, and on a long list that means creating components and native views for content the user may never scroll to, which slows rendering and raises memory use.
For short content that is not a problem, and ScrollView is the simpler tool. Use it when:
- The number of children is small and bounded, and will stay that way
- The content is a layout rather than a list, such as a form, a profile or a product page
- You want full control over what sits inside the scrollable area
import { ScrollView, Text } from 'react-native';
export function Settings({ options }) {
return (
<ScrollView>
{options.map((option) => (
<Text key={option.id}>{option.label}</Text>
))}
</ScrollView>
);
}One thing that catches people out: a ScrollView needs a bounded height to work. If it does not scroll, check that every parent view passes flex: 1 down the tree rather than setting a fixed height on the ScrollView itself.
ScrollView is not simply a worse FlatList. FlatList does real work to virtualise: it tracks which rows are visible, renders in batches and mounts and unmounts rows as you scroll. On a screen of ten fixed sections that machinery buys nothing, and a plain ScrollView is easier to lay out, easier to read and easier to debug.
How a React Native FlatList works
FlatList is built on VirtualizedList. It renders an initial batch of rows (initialNumToRender, 10 by default), then renders more in small batches as the user scrolls, and unmounts rows that move far enough outside a render window. That is why a list of thousands of rows can mount as quickly as a list of twenty.
It also brings the features a data list usually needs, so you do not build them yourself: pull to refresh, infinite scroll, separators, an empty state, headers and footers, and multiple columns through numColumns. A typical production list looks like this:
import { memo, useCallback } from 'react';
import { FlatList, Text, View } from 'react-native';
const Row = memo(function Row({ title }) {
return (
<View style={{ height: 64, justifyContent: 'center' }}>
<Text>{title}</Text>
</View>
);
});
export function OrdersList({ orders, refreshing, onRefresh, onLoadMore }) {
const renderItem = useCallback(({ item }) => <Row title={item.title} />, []);
return (
<FlatList
data={orders}
renderItem={renderItem}
keyExtractor={(item) => String(item.id)}
refreshing={refreshing}
onRefresh={onRefresh}
onEndReached={onLoadMore}
onEndReachedThreshold={0.5}
ListEmptyComponent={<Text>No orders yet</Text>}
/>
);
}Two details matter here. onEndReachedThreshold is measured in visible lengths of the list, so 0.5 asks for more data when the end is within half a screen; the default is 2. And because FlatList is a PureComponent, it only re-renders when its props change. If a row depends on something outside data, such as a selected id, pass it through extraData or the row will not update.
One more consequence of virtualisation: a row's internal state is not preserved once it scrolls out of the render window and is unmounted. If a row has a toggle, a text input or an expanded state the user expects to keep, store that state in the item data or in a parent store rather than in useState inside the row.
The React Native FlatList props that actually matter
FlatList is faster than ScrollView out of the box, but most complaints about it being slow come down to a handful of props left at their defaults on a list that needed them set.
| Prop | What it does | Set it when |
|---|---|---|
| keyExtractor | Returns a unique string key per row, used for caching and re-ordering. The default checks item.key, then item.id, then falls back to the index | Your items have no key or id field, or the id is a number you want as a string |
| getItemLayout | Tells FlatList each row's length and offset so it can skip measuring | Every row is the same height (or width, for a horizontal list) |
| initialNumToRender | How many rows render in the first batch. Default 10 | The first screen is slow to appear, or 10 rows does not fill it |
| windowSize | How many visible lengths of content stay rendered. Default 21: the screen plus up to 10 above and 10 below | Fast scrolling shows blank space (raise it) or memory is too high (lower it) |
| maxToRenderPerBatch | Rows rendered per batch while scrolling. Default 10 | You need to trade fill rate against responsiveness to taps |
| removeClippedSubviews | Detaches off-screen views from the native hierarchy. Default true on Android, false elsewhere | Long lists, after testing; the docs warn it can cause missing content |
If every row has a fixed height, getItemLayout is usually the first prop to add, because without it FlatList has to measure rows as it goes. Remember to include the separator height in the offset if you use ItemSeparatorComponent.
const ROW_HEIGHT = 64;
const getItemLayout = (data, index) => ({
length: ROW_HEIGHT,
offset: ROW_HEIGHT * index,
index,
});
<FlatList data={orders} renderItem={renderItem} getItemLayout={getItemLayout} />Common FlatList vs ScrollView mistakes
Using ScrollView for hundreds of items
This is the most frequent cause of a slow screen. Mapping an API response into a ScrollView works in development with twenty test records, then mounts every row at once when production returns hundreds. If the data comes from a server and has no fixed ceiling, start with FlatList even while the list is short.
Defining renderItem inline
Writing renderItem as an arrow function inside the JSX creates a new function on every render of the parent. The React Native performance guide recommends moving it out of the returned JSX and wrapping it in useCallback, as in the example above, and memoising the row component with memo so unchanged rows are skipped.
Missing or unstable keys
If your items have neither key nor id, FlatList falls back to the array index. Index keys break as soon as rows are inserted, removed or re-ordered, which shows up as the wrong row keeping its state or animation. Give every item a stable id and return it as a string from keyExtractor.
Nesting a FlatList inside a ScrollView
Putting a FlatList inside a ScrollView that scrolls the same way triggers this warning in development: "VirtualizedLists should never be nested inside plain ScrollViews with the same orientation because it can break windowing and other functionality - use another VirtualizedList-backed container instead." The outer ScrollView gives the list unlimited height, so it renders everything and you lose the virtualisation you chose it for.
The fix is almost always to invert it. Make the FlatList the outer component and move the content that sat above and below it into ListHeaderComponent and ListFooterComponent:
<FlatList
data={orders}
renderItem={renderItem}
keyExtractor={(item) => String(item.id)}
ListHeaderComponent={<ProfileHeader />}
ListFooterComponent={<Footer />}
/>The header scrolls with the list, the list stays virtualised, and there is only one vertical scroll on screen. A horizontal FlatList inside a vertical ScrollView, such as a carousel, does not trigger the warning, because the orientations differ.
Moving a ScrollView list to FlatList
Most of the lists I am asked to speed up started life as a map inside a ScrollView. The change is small and safe if you take it in order:
- Replace the ScrollView and the map with a FlatList, passing the array as data and the row markup as renderItem.
- Add a keyExtractor that returns a stable id as a string, and remove the key you were setting on each mapped child.
- Move anything that sat above or below the list into ListHeaderComponent and ListFooterComponent, so there is only one scroll container.
- Extract the row into its own component wrapped in memo, and wrap renderItem in useCallback.
- If a row reads state that is not in data, pass that value to extraData.
- Add getItemLayout if rows are a fixed height, then test on a lower-end Android device rather than only in a simulator.
What about SectionList?
SectionList is the built-in choice when your data falls into groups with headings: contacts by letter, orders by date, bookings by month. It is virtualised in the same way, takes the same performance props, and adds renderSectionHeader. Section headers stick to the top by default on iOS and not on Android; stickySectionHeadersEnabled controls that.
<SectionList
sections={[
{ title: 'Today', data: todayOrders },
{ title: 'Earlier', data: olderOrders },
]}
keyExtractor={(item) => String(item.id)}
renderItem={({ item }) => <Row title={item.title} />}
renderSectionHeader={({ section }) => <Text>{section.title}</Text>}
/>When FlatList is still not fast enough: FlashList
FlashList is Shopify's open-source list component with a FlatList-style API. Instead of mounting and unmounting rows, it recycles row components as you scroll. The current major version, v2, was rebuilt for React Native's New Architecture and will not run on the old one; projects still on the old architecture need v1. Unlike v1, v2 does not need size estimates, so the old estimatedItemSize prop is no longer used.
import { FlashList } from '@shopify/flash-list';
<FlashList
data={orders}
renderItem={renderItem}
keyExtractor={(item) => String(item.id)}
/>Its docs strongly recommend a valid keyExtractor, and getItemType helps recycling when a list mixes different row types. Because rows are reused, any useState inside a row can carry over to a different item, so check row state when you migrate. It is also one more dependency to keep current. Most lists that feel slow on FlatList improve once the mistakes above are fixed, so fix those and measure before switching.
Frequently asked questions
How many items before I should switch from ScrollView to FlatList?
There is no official number. The better question is whether the count is bounded. A settings screen has a fixed set of rows, so ScrollView is fine. Anything driven by an API has no ceiling, so use FlatList from the start, even while the list is short.
Why is my FlatList showing blank space while I scroll?
Rows are rendering more slowly than you are scrolling. Add getItemLayout if rows are a fixed height, memoise the row component, move renderItem into useCallback, use smaller images, and consider raising windowSize or maxToRenderPerBatch, accepting the extra memory and JavaScript work.
Can FlatList have sticky headers and pull to refresh?
Yes. refreshing and onRefresh add pull to refresh, and FlatList accepts stickyHeaderIndices because it inherits ScrollView props. For headers that belong to groups of rows, SectionList is the cleaner tool.
Summary: which to use
| Your situation | Use |
|---|---|
| A settings screen with a dozen rows | ScrollView |
| A form or detail page that scrolls | ScrollView |
| A feed, a chat, a product list | FlatList |
| Anything paginated from an API | FlatList |
| Grouped rows with section headers | SectionList |
| Very long or complex lists still dropping frames after tuning, on the New Architecture | Consider FlashList |
Choosing between FlatList and ScrollView is a decision about how the screen will behave in production, not about syntax. Pick ScrollView for short, fixed content and FlatList for data, and the most common performance problems never appear.
If you would rather have the lists, and the app around them, built and tuned for you, see my mobile app development in Dubai services.