Twenty items in a feed feels perfect. The scroll is butter. Your client is happy. Then you ship to production, the data arrives, and suddenly you are staring at two thousand rows. The UI starts to hitch. Memory creeps up until the OS pulls the plug. In desperation, some developers wrap everything in a ScrollView and call it a day. That decision usually births three new bugs for every one it solves.

Lists are the performance bottleneck that defines how users perceive your React Native app. Get them right and the app feels native. Get them wrong and even the most beautiful screen becomes a slog. The root cause is usually a mismatch between the component you chose and the work you are asking the JavaScript and UI threads to perform. React Native runs on two tracks. Your logic lives on the JS thread while painting happens on the native UI thread. When you render a massive list incorrectly, both threads start drowning in layout calculations, re-renders, and memory allocations. The result is dropped frames, blank flashes, and eventually a crash.

Pick the Right Tool

Choosing a list component should be a deliberate architectural decision, not a reflex.

ScrollView is the simplest option. It takes every child you give it, mounts every single one into memory immediately, and hands the whole pile to the native scroll engine. That is exactly what you want for short, fixed content such as a settings screen, a login form, or a static product detail page with ten sections. It is predictable and easy to style. The catch is that there is no virtualization. If you feed it two thousand items, it will obediently create two thousand native views. Do not use ScrollView for large or dynamic data sets. Think of it as a framed poster, not a library shelf.

FlatList is the workhorse for long, uniform feeds. It virtualizes content, which means it only mounts rows that are currently visible or near the viewport. As the user scrolls, FlatList unmounts cells that leave the screen and recycles them for incoming data. This keeps memory flat regardless of how large your array grows. If you are building a social timeline, a notification center, or any continuously scrolling collection of similar cards, FlatList is the default correct choice.

SectionList is FlatList with a sense of organization. Use it when your data arrives in grouped buckets, such as an address book sorted alphabetically, a workout log split by date, or an invoice list organized by month. It renders sticky section headers and handles the grouping logic for you. Under the hood it uses the same virtualization engine as FlatList, so you get identical memory benefits with the added structure of titled partitions.

FlashList steps in when you need to squeeze every last frame out of the device. Built on top of the RecyclerListView ecosystem, it recycles views more aggressively than FlatList and aims to maintain sixty frames per second even on mid-range hardware. If you are building a high-volume chat interface, a product catalog with rapid scroll velocity, or any screen where smoothness is a competitive advantage, FlashList is worth the added dependency. It is not necessary for every screen, but for feeds that define the core experience, the performance delta is noticeable.

Common Performance Killers

There are three usual suspects when a list starts to drag.

Mounting too many React trees at once is the most dramatic failure. When every row is a complex component tree, the initial render can block the JS thread long enough to produce a blank white screen or an ugly delayed first paint. The user opens the app and waits. Even after the initial load, heavy rows make scroll initialization sluggish because the first few frames are consumed by setup work.

Too much work per frame shows up as stuttering during scroll. You have a budget of roughly sixteen milliseconds per frame to keep animations smooth. If a row component runs expensive calculations, parses dates on the fly, or performs deep object comparisons inside render, you blow that budget. The UI thread drops frames and the user feels a jolt.

Excessive memory use is the silent killer. Every native view costs RAM. Add large unoptimized images, drop shadows on every card, or nested touchables and the footprint multiplies. On iOS the system may terminate your app without warning. On Android the user watches the lag build until the app feels unusable.

Optimization Checklist

Small strategic habits separate a list that merely works from one that flies.

Use stable keys. Always pass a real identifier from your data set to the key prop. Never use the array index. If your list reorders, filters, or appends items, an index-based key tricks React into pairing the wrong data with the wrong recycled component. That mistake triggers unnecessary unmounts, state mismatches, and cascading re-renders. A proper ID tells React exactly which row moved where.

Memoize rows. Wrap your row component in React.memo so that it only re-renders when its props actually change. Without this guard, any parent state update can trigger a render pass across every visible row, even if their data is identical. In a long list that churns, those wasted cycles add up fast.

Keep renderItem stable. Avoid defining a new function directly inside the renderItem prop on every parent render. An inline arrow function like renderItem={({ item }) => <Row data={item} />} creates a new reference each time the parent updates. FlatList sees a changed prop and recycles the row unnecessarily. Define the render function outside the component or memoize it with useCallback so the reference stays stable.

Use getItemLayout whenever possible. If your rows have a fixed or predictable height, tell FlatList exactly what it is. This prop allows the list to skip expensive native measurement calls. Instead of measuring each cell after mount, the list calculates position mathematically. The difference is especially sharp on lists with hundreds or thousands of items, where onLayout chatter can bring the JS thread to its knees.

Optimize images aggressively. Unbounded images are list poison. Always set explicit width and height so the native layer reserves space before the image decodes. For remote images, use a caching library such as Expo Image or an equivalent that handles memory caching, disk persistence, and format optimization. The default React Native Image component works for prototypes, but production feeds need more control over memory and loading states.

Avoid nesting scroll containers. Never place a vertical FlatList inside a vertical ScrollView. The parent ScrollView captures all scroll events and disrupts the child FlatList's ability to measure its viewport. Virtualization breaks because FlatList no longer knows which rows should be visible. The result is that every row mounts anyway, defeating the entire purpose of virtualization. If you need a header above a list, use FlatList's own ListHeaderComponent prop. If you need complex sticky behavior, use a SectionList or FlashList with the appropriate header configuration.

The Golden Rule

If the content is small and finite, let ScrollView handle it. If the content grows with user-generated data or remote pagination, use a virtualized list. When the list is the centerpiece of the app and users will scroll for minutes at a time, reach for FlashList.

Here is one last truth that is easy to forget. Boring rows scroll fast. The lighter your row component, the smoother your list. Strip away nested navigations, heavy computations, and gratuitous animations from the individual row. Keep the markup flat, the logic thin, and the images sized. A list lives or dies by the cumulative weight of what it renders. Make each row cheap, and the list will feel expensive in the best possible way.