Picture this: You’ve just spent weeks perfecting your React Native app. The UI looks stunning, features work flawlessly, and you’re ready to show it off. Then you open that one screen with a long product list… and everything falls apart.
The smooth scrolling becomes choppy. Frames drop like flies. Your beautiful app suddenly feels like it’s running through molasses. Sound familiar? You’re not alone — and more importantly, you’re not stuck with this problem forever.
Every React Native developer has lived this nightmare. You build something amazing, only to watch FlatList performance crumble under the weight of real data. But here’s where the story takes an unexpected turn: there’s a solution that doesn’t just patch the problem — it eliminates it entirely.
Enter React Native FlashList, Shopify’s answer to the age-old question: “Why can’t mobile lists just work like they’re supposed to?”
The FlatList Performance Problem Every Developer Faces
Before we dive into the hero of our story, let’s understand exactly why FlatList leaves you frustrated in the first place. Because once you see what’s happening under the hood, FlashList’s approach will blow your mind.

The Mount-Unmount Death Spiral
FlatList relies on a component called VirtualizedList, and its strategy seems logical at first: only render items currently visible on screen, plus a small buffer. This React Native list optimization technique, called virtualization, keeps memory usage reasonable.
But here’s where things get problematic…
Every time you scroll and a new item enters the viewport, FlatList doesn’t just show it — it completely recreates it from scratch. Even if you’ve scrolled past that exact same item five times before, FlatList will mount a brand new component, run all your render logic, and then unmount it again when you scroll away.
Let me show you what this looks like in practice:
import React, { useState } from "react";
import { FlatList, Text, View } from "react-native";
export default function App() {
const [data] = useState(
Array.from({ length: 5000 }, (_, i) => `Item ${i + 1}`)
);
const renderItem = ({ item }) => {
console.log("Rendering:", item); // This fires EVERY time item enters view
return (
<View style={{ padding: 16, borderBottomWidth: 1 }}>
<Text>{item}</Text>
</View>
);
};
return (
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={(item) => item}
/>
);
}Run this code and watch your console. You’ll see something that might shock you: every single scroll triggers a flood of “Rendering” messages. Each time an item re-enters the viewport, your renderItem function runs again, even though nothing has changed.
On a small list, you might not notice. But scale this to 5,000 items, and you’ll watch your JavaScript thread spike and your mobile app scrolling performance tank.
The React.memo Band-Aid (That Doesn’t Really Work)
Some developers try to solve this with React.memo, thinking they can skip unnecessary re-renders:
const ListItem = React.memo(({ value }) => {
console.log("Rendering:", value);
return (
<View style={{ padding: 12, borderBottomWidth: 1, borderColor: "#ccc" }}>
<Text>{value}</Text>
</View>
);
});This helps… but only while the component stays mounted. The moment an item scrolls off-screen, FlatList unmounts it completely. When it comes back into view, FlatList mounts it again, triggering a full render regardless of your React.memo optimization.
The harsh reality? React.memo can’t save you from FlatList’s fundamental design: the constant mount/unmount cycle that’s baked into how it operates.
This is where our story takes a dramatic turn…

How React Native FlashList Changes Everything
FlashList doesn’t just optimize FlatList’s approach — it completely reimagines it. Instead of fighting React’s reconciliation process, FlashList bypasses it entirely using a technique that mirrors what native platforms have been doing for years.
The Game-Changing Difference: View Recycling
Here’s the breakthrough moment: Instead of unmounting and remounting items when they go off-screen, FlashList recycles the actual views.
Think of it like this:
- FlatList approach: “Need a new item? Let me create a brand new component, run all the render logic, mount it, style it… oh, you scrolled past it? Let me throw it all away and start over.”
- FlashList approach: “Need a new item? Let me grab this existing view, update its content, and you’re done.”
The difference is staggering.

Four Technical Breakthroughs That Make FlashList Unstoppable
1. Smart View Recycling When an item scrolls off-screen, its view stays in memory. FlashList simply updates the content without creating new components. No mounting, no unmounting, no performance cliff.
2. React Reconciliation Bypass Because views are reused, React never needs to diff component trees or update the virtual DOM. The expensive reconciliation process that kills FlatList performance? FlashList skips it entirely.
3. Intelligent Pre-Rendering FlashList measures item sizes ahead of time and renders them in memory before you even scroll to them. Fast scrolling doesn’t cause layout jumps because FlashList already knows exactly how much space everything needs.
4. Native-Level Windowing Like FlatList, FlashList keeps a limited set of items active. But instead of destroying components, it swaps data in and out of recycled views — just like RecyclerView on Android or UITableView on iOS.
The Technical Deep-Dive: How FlashList Outsmarts React
Understanding exactly how FlashList bypasses React’s performance bottlenecks will change how you think about React Native performance optimization forever.
The Native View Pool Strategy
FlashList maintains a pool of native views that are never destroyed during normal scrolling. Here’s the magic:
- View Pool Management: Each visible item gets a native view instance that survives scrolling

- Direct Data Binding: Instead of passing new props through React (triggering reconciliation), FlashList directly updates native view content

- Layout Caching: Item measurements happen once and get cached, eliminating repeated layout calculations

The performance difference is dramatic:
- FlatList: setState → re-render → React reconciliation → native update
- FlashList: Direct native view update (skipping the entire React pipeline)
Why This Approach Is Revolutionary
This isn’t just a minor optimization — it’s a fundamental rethinking of how lists should work in React Native. By treating the native layer as the source of truth instead of React components, FlashList achieves mobile app scrolling performance that rivals native apps.
Mastering FlashList Props: Your Performance Toolkit
Now that you understand the “why” behind FlashList’s speed, let’s explore the “how” — the specific props that unlock its full potential.
1. estimatedItemSize: Your Performance Foundation
This prop is FlashList’s crystal ball. It doesn’t need perfect accuracy, but giving it a good estimate helps it make smart decisions about rendering and layout.
<FlashList
data={products}
renderItem={renderProduct}
estimatedItemSize={120} // Average height of your items
/>Pro tips for getting this right:
- Uniform items? Use their exact size
- Variable heights? Calculate the average or median
- Stuck between two numbers? Go smaller
- Use Element Inspector to fine-tune
⚠️ Don’t ignore the warning! If you skip this prop, FlashList will nag you — and for good reason. This single prop can make or break your performance.
2. getItemType: Smart Recycling for Complex Lists
Not all list items are created equal. Headers, products, ads, and content cards all have different structures. The getItemType prop helps FlashList recycle intelligently:
<FlashList
data={feedData}
renderItem={renderFeedItem}
getItemType={(item) => item.type} // 'header', 'product', 'ad', etc.
estimatedItemSize={100}
/>This ensures that a product card never accidentally gets recycled as an advertisement, maintaining both performance and visual consistency.
Pro tips for getting this right:
- Use it if you have multiple item types.
- Return undefined for items where you don’t care.
- Keep it fast — this function runs a lot.
- overrideItemLayout: When You Know Better
Sometimes you have exact measurements for your items. Instead of letting FlashList guess, you can provide precise dimensions:
<FlashList
data={gridData}
renderItem={renderGridItem}
overrideItemLayout={(layout, item, index) => {
layout.size = item.featured ? 200 : 100;
layout.span = item.featured ? 2 : 1;
}}
/>Why does this matter?
- More precise scrollToIndex and initialScrollIndex.
- Better accuracy than estimatedItemSize.
- Grid layouts where some items span multiple columns.
Building Recycling-Friendly Components: The Art of Performance
FlashList’s recycling magic only works if your item components play along. Here’s how to write components that make FlashList shine instead of struggle.
The Golden Rule: Keep Components Stateless and Fast
Since components get recycled with different data, they should be pure functions of their props:
// ✅ GOOD: Pure, fast, recycling-friendly
const ProductItem = React.memo(({ product }) => (
<View style={styles.container}>
<Text>{product.name}</Text>
<Text>${product.price}</Text>
</View>
));
// ❌ BAD: Stateful, heavy calculations every recycle
const BadProductItem = ({ product }) => {
const [count, setCount] = useState(0);
const processedName = product.name.split("").reverse().join(""); // Expensive!
return (
<View>
<Text>{processedName}</Text>
<Text>{count}</Text>
</View>
);
};The Key Prop Trap (And How to Avoid It)
Here’s a mistake that breaks recycling every time: adding key props inside your item components.
// ❌ This breaks recycling!
const UserRow = ({ user }) => {
return (
<View key={user.id}>
{/\* FlashList manages keys internally \*/}
<Text key={user.id}>{user.name}</Text> {/\* This forces new elements \*/}
</View>
);
};
// ✅ Better: Let FlashList handle keys
const UserRow = ({ user }) => {
return (
<View>
<Text>{user.name}</Text>
</View>
);
};- There might be cases where React forces you to use key prop, such as when using map. In such circumstances, ensure that the key is not tied to the item prop in any way, so the keys don’t change when recycling.
const MemberList = ({ item }) => {
return (
<>
{item.members.map((member) => (
<Text key={member.id}>{member.name}</Text>
))}
</>
);
};- If we wrote our item component like this, the Text component would need to be re-created. Instead, we can do the following:
const MemberList = ({ item }) => {
return (
<>
{item.members.map((member, index) => (
<Text key={index}>{member.name}</Text>
))}
</>
);
};- Although using index as a key in map is not recommended by React, in this case since the data is derived from the list’s data, the items will update correctly.
Memoization: Your Performance Insurance Policy
For expensive calculations that don’t change often, use useMemo:
const ProductItem = React.memo(({ product }) => {
const discountPercentage = useMemo(
() => calculateDiscount(product.originalPrice, product.salePrice),
[product.originalPrice, product.salePrice]
);
return (
<View>
<Text>{product.name}</Text>
<Text>{discountPercentage}% off!</Text>
</View>
);
});Debugging the Dreaded Blank Spaces
Every FlashList developer eventually encounters them: those frustrating blank spaces that appear during fast scrolling. But here’s the thing — they’re not bugs, they’re performance signals telling you exactly what needs fixing.

Why Blank Spaces Happen (And What They Mean)
When you see blank space, FlashList is essentially saying: “I don’t have the next items ready to render yet.” This happens when:
- Your estimatedItemSize is way off
- Item components are too heavy to render quickly
- You’re scrolling faster than FlashList can keep up
The Three-Step Fix
Step 1: Dial in your estimatedItemSize Use the React Native debugger to measure your actual item heights, then adjust:
<FlashList
data={data}
renderItem={renderItem}
estimatedItemSize={85} // Fine-tuned from actual measurements
/>Step 2: Optimize your item components Profile your renderItem function. If it takes more than a few milliseconds, you've found your bottleneck:
const FastItem = React.memo(({ item }) => {
// Keep this function lightning fast
return (
<View style={styles.itemContainer}>
<Text>{item.title}</Text>
</View>
);
});Step 3: Use getItemType for complex lists Help FlashList recycle more efficiently:
<FlashList
data={mixedData}
renderItem={renderMixedItem}
getItemType={(item) => item.type}
estimatedItemSize={100}
/>Measuring Success: Performance Monitoring That Actually Matters
Building fast lists isn’t about gut feelings — it’s about data. FlashList gives you two crucial metrics that tell you exactly how your list performs in the real world.
Load Time: Measuring First Impressions
The onLoad callback tells you precisely how long users wait to see content:
const MyList = () => {
import React, { useCallback } from 'react';
import { FlashList } from '@shopify/flash-list';
const MyComponent = () => {
const onLoadListener = useCallback(({ elapsedTimeInMs }) => {
console.log("Sample List load time", elapsedTimeInMs);
// Track this metric in your analytics
}, []);
return (
<FlashList
{...props}
onLoad={onLoadListener}
/>
);
};What good numbers look like:
- Under 100ms: Excellent, users won’t notice
- 100–300ms: Good, feels responsive
- Over 300ms: Investigate and optimize
Blank Space Tracking: Quantifying Smoothness
The useBlankAreaTracker hook gives you hard data on scrolling performance:
import { useBlankAreaTracker } from "@shopify/flash-list";
function MyListComponent() {
const ref = useRef(null);
const [blankAreaResult, onBlankArea] = useBlankAreaTracker(ref);
useEffect(() => {
return () => {
// this console log will only run when the component unmounts.
// giving you correct results.
console.log("Blank area stats:", blankAreaResult);
// blankAreaResult.cumulativeBlankArea = total pixels of blank space
// blankAreaResult.maxBlankArea = worst single gap
};
}, [blankAreaResult]);
return (
<FlashList
ref={ref}
onBlankArea={onBlankArea}
data={data}
renderItem={renderItem}
estimatedItemSize={80}
/>
);
}Benchmark targets:
- maxBlankArea under 50px: Users won't notice gaps
- cumulativeBlankArea under 200px: Smooth scrolling experience
Why FlashList Should Be Your New Default (Not Your Backup Plan)
Here’s the truth that every React Native developer needs to hear: FlatList was never designed for today’s data-heavy, infinite-scroll world.
When FlatList was created, most lists were small and simple. But today’s apps demand more:
- Infinite product catalogs
- Real-time social feeds
- Complex mixed-content lists
- Smooth 60fps scrolling on budget devices
FlatList’s mount/unmount architecture simply can’t handle these requirements without compromise. No amount of optimization can fix its fundamental limitations.
The Migration Moment
The switch from FlatList to FlashList takes minutes, not hours:
// Before
import { FlatList } from "react-native";
// After
import { FlashList } from "@shopify/flash-list";Add estimatedItemSize, and you're done. Your users will notice the difference immediately.
The Bottom Line: Performance That Scales
Every React Native developer reaches the same crossroads: stick with FlatList and accept performance compromises, or embrace FlashList and deliver the smooth, responsive experience your users deserve.
The choice seems obvious, but here’s what makes it definitive: FlashList doesn’t just perform better — it gets better as your app grows. While FlatList degrades with more data, FlashList maintains consistent performance whether you’re showing 100 items or 100,000.
Your users don’t care about your technical constraints. They care about apps that feel fast, responsive, and polished. FlashList isn’t just a library upgrade — it’s your competitive advantage.
Stop treating smooth scrolling as a nice-to-have. Make FlashList your new default, and make performance problems a thing of the past.
Ready to transform your React Native lists? Install FlashList today and experience the difference that proper view recycling makes. Your users (and your app store ratings) will thank you.