Module 5: Lists and Performance
Why ScrollView Isn't Enough
Understanding the performance cliff that every React Native developer hits
🎯 Learning Objectives
- Understand why ScrollView works fine for small lists but fails at scale
- Identify the specific performance problems that occur with large datasets
- Learn what virtualization means and why it's essential for mobile
- Recognize the symptoms of ScrollView performance issues in your own apps
- Preview how FlatList solves these problems through smart rendering
The ScrollView Trap
Every React Native developer falls into the same trap. You're building your app, everything works beautifully, and then you need to display a list of items. "Easy," you think, "I'll just use ScrollView like I did for my profile page." You wrap your items in a ScrollView, map over your data, and... it works! Twenty items render perfectly. You move on.
Then your app goes to production. Users have real data now. Hundreds of items. Thousands, maybe. And suddenly, your app crawls. It stutters. It freezes. Users complain. You've hit the ScrollView wall.
📖 The Core Problem
ScrollView renders ALL of its children at once, regardless of whether they're visible on screen. This approach simply doesn't scale. A list of 1,000 items means 1,000 components created, mounted, and held in memory—even though users can only see maybe 10 at a time.
This isn't a React Native bug or limitation you can work around with clever code. It's a fundamental architectural decision that makes ScrollView perfect for some use cases and completely wrong for others. Understanding this distinction is crucial for building performant mobile apps.
ScrollView Under the Hood
To understand why ScrollView fails at scale, let's look at what actually happens when you render a list with it:
// This innocent-looking code hides a performance bomb
import { ScrollView, View, Text } from 'react-native';
function ContactList({ contacts }) {
return (
<ScrollView>
{contacts.map(contact => (
<View key={contact.id} style={styles.contactCard}>
<Text style={styles.name}>{contact.name}</Text>
<Text style={styles.email}>{contact.email}</Text>
<Text style={styles.phone}>{contact.phone}</Text>
</View>
))}
</ScrollView>
);
}
When React Native encounters this code, here's the sequence of events:
sequenceDiagram
participant App
participant React
participant Bridge
participant Native
App->>React: Render ContactList
React->>React: Create VDOM for ALL contacts
loop For each contact (1000x)
React->>Bridge: Create native View
React->>Bridge: Create native Text (name)
React->>Bridge: Create native Text (email)
React->>Bridge: Create native Text (phone)
Bridge->>Native: Instantiate components
Native->>Native: Layout calculation
Native->>Native: Allocate memory
end
Native->>Native: Display first 10 items
Note over Native: 990 items invisible but fully rendered
Notice the problem? The native side creates and lays out every single item before displaying anything. For a list of 1,000 contacts with 3 text elements each, that's 4,000 native components created, positioned, and held in memory.
💡 The Web Comparison
On the web, browsers are incredibly optimized for rendering large DOM trees. They use techniques like lazy painting, layer compositing, and incremental layout. Mobile native views don't have these same optimizations—each view is a real, heavyweight object with its own memory allocation and rendering overhead.
The Performance Cliff
ScrollView performance doesn't degrade gracefully. It works fine, works fine, works fine... then suddenly doesn't. This is the "performance cliff" that catches developers off guard.
The cliff typically appears somewhere between 50-200 items, depending on the complexity of each item, the device's hardware, and what else is happening in your app. But make no mistake—it will appear.
⚠️ The Deceptive Development Experience
During development, you're often testing with small datasets on powerful devices (your development machine or a recent phone). The cliff might not appear until real users with older phones and real data start using your app. Always test with realistic data volumes!
Memory: The Silent Killer
Frame rate drops are visible—your app stutters and users notice immediately. But there's an even more dangerous problem lurking: memory consumption. ScrollView holds all rendered items in memory, and mobile devices have strict limits.
// Let's calculate the memory impact
// A typical contact card might include:
const ContactCard = ({ contact }) => (
<View style={styles.card}> {/* ~0.5KB base View */}
<Image {/* ~2-5KB for cached image */}
source={{ uri: contact.avatar }}
style={styles.avatar}
/>
<View style={styles.info}> {/* ~0.5KB */}
<Text style={styles.name}> {/* ~0.3KB */}
{contact.name}
</Text>
<Text style={styles.email}> {/* ~0.3KB */}
{contact.email}
</Text>
</View>
</View>
);
// Conservative estimate: ~4KB per contact card
// 1,000 contacts = ~4MB just for the list
// 10,000 contacts = ~40MB
// Plus JavaScript objects, layout cache, etc.
Mobile devices typically have 2-6GB of RAM total, shared across all running apps. iOS and Android will terminate apps that use too much memory, often without warning. Users experience this as random crashes.
🚨 Real Crash Scenario
A social media app rendering a feed with ScrollView: 500 posts × (post content + images + interaction buttons) = memory spike. User scrolls through feed, memory grows, iOS sends memory warning, app doesn't respond fast enough, terminated. User sees their phone's home screen with no explanation.
Real-World Symptoms
How do you know if your app is suffering from ScrollView performance issues? Here are the telltale signs:
🔍 Symptom Checklist
| Symptom | What's Happening | Severity |
|---|---|---|
| Slow initial load | All items rendering before display | ⚠️ Medium |
| Scroll jank/stutter | JS thread blocked during scroll | 🔴 High |
| Touch delay | Main thread overwhelmed | 🔴 High |
| Random crashes | Memory limit exceeded | 🚨 Critical |
| Hot device / battery drain | Excessive rendering work | ⚠️ Medium |
| App feels "heavy" | General resource exhaustion | ⚠️ Medium |
Let's create a simple test to see the problem in action:
// Performance test component - try this with different list sizes
import React, { useEffect, useState } from 'react';
import { ScrollView, View, Text, StyleSheet } from 'react-native';
// Generate fake data
const generateItems = (count: number) =>
Array.from({ length: count }, (_, i) => ({
id: i,
title: `Item ${i + 1}`,
description: `This is the description for item number ${i + 1}`,
timestamp: new Date().toISOString(),
}));
export function ScrollViewPerformanceTest() {
const [items, setItems] = useState<any[]>([]);
const [renderTime, setRenderTime] = useState<number | null>(null);
useEffect(() => {
const start = performance.now();
// Try changing this number: 50, 100, 500, 1000
setItems(generateItems(500));
// Measure time after render completes
requestAnimationFrame(() => {
requestAnimationFrame(() => {
setRenderTime(performance.now() - start);
});
});
}, []);
return (
<View style={styles.container}>
{renderTime && (
<View style={styles.metrics}>
<Text style={styles.metricsText}>
Rendered {items.length} items in {renderTime.toFixed(0)}ms
</Text>
</View>
)}
<ScrollView>
{items.map(item => (
<View key={item.id} style={styles.item}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.description}>{item.description}</Text>
<Text style={styles.timestamp}>{item.timestamp}</Text>
</View>
))}
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
metrics: {
padding: 16,
backgroundColor: '#ffeb3b',
},
metricsText: {
fontWeight: 'bold',
textAlign: 'center',
},
item: {
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
title: {
fontSize: 16,
fontWeight: 'bold',
},
description: {
fontSize: 14,
color: '#666',
marginTop: 4,
},
timestamp: {
fontSize: 12,
color: '#999',
marginTop: 4,
},
});
Run this with different item counts and observe how render time scales. On a mid-range device, you might see results like:
50 items: ~50ms ✅
100 items: ~120ms ⚠️
500 items: ~800ms 🔴
1000 items: ~2000ms 🚨
5000 items: App may freeze or crash 💀
Virtualization: The Solution
The solution to this problem has a name: virtualization (also called "windowing"). It's not a React Native invention—this pattern has been solving list performance problems for decades, from desktop applications to web frameworks.
📖 Virtualization Defined
Virtualization is a rendering technique where only the items currently visible on screen (plus a small buffer) are actually rendered. As the user scrolls, items leaving the viewport are destroyed and recycled to create items entering the viewport.
Think of it like a theater production. A traditional approach (ScrollView) would build the entire set for every scene before the play starts—expensive and space-consuming. Virtualization is like having stagehands quickly swap set pieces as scenes change—only what's currently on stage exists.
flowchart TB
subgraph ScrollView["ScrollView Approach"]
SV1[Item 1] --- SV2[Item 2] --- SV3[Item 3] --- SV4[Item 4]
SV4 --- SV5[Item 5] --- SV6[Item 6] --- SV7[Item 7]
SV7 --- SV8[Item 8] --- SV9[Item 9] --- SV10[Item 10]
SV10 --- SVN[... Item 1000]
style SV1 fill:#ffcdd2
style SV2 fill:#ffcdd2
style SV3 fill:#ffcdd2
style SV4 fill:#ffcdd2
style SV5 fill:#ffcdd2
style SV6 fill:#ffcdd2
style SV7 fill:#ffcdd2
style SV8 fill:#ffcdd2
style SV9 fill:#ffcdd2
style SV10 fill:#ffcdd2
style SVN fill:#ffcdd2
end
subgraph VList["Virtualized List Approach"]
direction TB
Empty1[" "] ~~~ V3[Item 3]
V3 --- V4[Item 4]
V4 --- V5[Item 5]
V5 --- V6[Item 6]
V6 --- V7[Item 7]
V7 ~~~ Empty2[" "]
style V3 fill:#c8e6c9
style V4 fill:#c8e6c9
style V5 fill:#c8e6c9
style V6 fill:#c8e6c9
style V7 fill:#c8e6c9
style Empty1 fill:none,stroke:none
style Empty2 fill:none,stroke:none
end
Note1["All 1000 items
in memory"] --> ScrollView
Note2["Only ~5-10 items
in memory"] --> VList
The key insight is that users can only see a small portion of any list at a time. A phone screen might display 8-12 items. Why render the other 988?
The Virtualization Tradeoff
Virtualization isn't free—there's overhead in calculating what's visible and managing the recycling process. But this overhead is constant, regardless of list size. Whether your list has 100 items or 100,000, the rendering work is the same.
✅ The Performance Guarantee
With virtualization, list performance becomes O(1) instead of O(n). Doubling your data doesn't double your render time. This is why apps like Twitter, Instagram, and every email client you've ever used rely on virtualized lists.
The Rendering Window Concept
To understand how virtualization works in practice, let's visualize the "rendering window"—the portion of your list that actually exists as rendered components at any given moment.
The rendering window consists of three zones:
- Visible Zone: Items currently on screen. These must be rendered.
- Buffer Zone: A few items above and below the visible area, pre-rendered for smooth scrolling.
- Virtual Zone: Everything else—these items exist only as data, not as rendered components.
As the user scrolls, items move between zones. Items entering the buffer zone are created; items leaving are destroyed (or more precisely, recycled). This constant recycling is what keeps memory usage stable.
// Conceptual pseudocode of virtualization
function VirtualizedList({ data, renderItem }) {
const [scrollPosition, setScrollPosition] = useState(0);
const [windowHeight, setWindowHeight] = useState(0);
// Calculate which items should be rendered
const visibleRange = useMemo(() => {
const itemHeight = 50; // Assume fixed height
const bufferSize = 5; // Extra items above/below
const firstVisible = Math.floor(scrollPosition / itemHeight);
const lastVisible = Math.ceil((scrollPosition + windowHeight) / itemHeight);
return {
start: Math.max(0, firstVisible - bufferSize),
end: Math.min(data.length - 1, lastVisible + bufferSize),
};
}, [scrollPosition, windowHeight, data.length]);
// Only render items in the visible range
const itemsToRender = data.slice(visibleRange.start, visibleRange.end + 1);
return (
<ScrollContainer onScroll={setScrollPosition}>
{/* Spacer for items above the window */}
<Spacer height={visibleRange.start * itemHeight} />
{/* Actually rendered items */}
{itemsToRender.map((item, index) =>
renderItem(item, visibleRange.start + index)
)}
{/* Spacer for items below the window */}
<Spacer height={(data.length - visibleRange.end - 1) * itemHeight} />
</ScrollContainer>
);
}
💡 The Spacer Trick
Notice the spacers in the pseudocode. Virtualized lists maintain the correct scroll position and scrollbar size by adding empty space where unrendered items would be. This creates the illusion that all items exist, while only rendering what's needed.
Preview: Enter FlatList
React Native's answer to the ScrollView performance problem is FlatList. It implements the virtualization pattern we just discussed, with a thoughtfully designed API that handles the complexity for you.
Here's how our problematic contact list transforms with FlatList:
// Before: ScrollView (problematic)
import { ScrollView, View, Text } from 'react-native';
function ContactListOld({ contacts }) {
return (
<ScrollView>
{contacts.map(contact => (
<View key={contact.id} style={styles.contactCard}>
<Text>{contact.name}</Text>
<Text>{contact.email}</Text>
</View>
))}
</ScrollView>
);
}
// After: FlatList (performant)
import { FlatList, View, Text } from 'react-native';
function ContactListNew({ contacts }) {
return (
<FlatList
data={contacts}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<View style={styles.contactCard}>
<Text>{item.name}</Text>
<Text>{item.email}</Text>
</View>
)}
/>
);
}
The transformation is minimal—you're essentially moving from a "render everything" pattern to a "render on demand" pattern—but the performance impact is dramatic:
graph LR
subgraph Before["ScrollView: 1000 contacts"]
B1["Initial render: ~2000ms"]
B2["Memory: ~40MB"]
B3["Scroll: Janky"]
end
subgraph After["FlatList: 1000 contacts"]
A1["Initial render: ~50ms"]
A2["Memory: ~0.5MB"]
A3["Scroll: 60 FPS"]
end
Before -->|"Same data
Different approach"| After
style B1 fill:#ffcdd2
style B2 fill:#ffcdd2
style B3 fill:#ffcdd2
style A1 fill:#c8e6c9
style A2 fill:#c8e6c9
style A3 fill:#c8e6c9
We'll dive deep into FlatList in the next lesson. For now, understand that it solves the fundamental problem we've identified: rendering only what's necessary.
✅ Key API Differences
| ScrollView | FlatList |
|---|---|
children (JSX) |
data prop (array) |
.map() in render |
renderItem prop (function) |
key on each item |
keyExtractor function |
| You manage everything | FlatList manages virtualization |
Beyond FlatList
React Native provides several virtualized list components for different use cases:
- FlatList: The workhorse. Handles most list scenarios.
- SectionList: For grouped data with section headers (like a contacts app with alphabetical sections).
- VirtualizedList: The base component that FlatList and SectionList are built on. Use when you need custom behavior.
We'll cover all of these throughout this module, but FlatList is where 90% of your list needs will be met.
When ScrollView Is Actually Fine
After all this doom and gloom about ScrollView, let's be clear: ScrollView is a great component. It's just not the right tool for long, dynamic lists. There are many legitimate use cases where ScrollView is the correct choice.
✅ Good Use Cases for ScrollView
- Forms: A signup form with 10-15 input fields
- Settings screens: A page of toggles and options (typically under 30 items)
- Product detail pages: Images, descriptions, specs, reviews preview
- Article content: Long-form text with embedded images
- Dashboard layouts: Multiple cards/widgets on one screen
- Modal content: Scrollable content in a popup
- Fixed, known content: Help pages, about screens, onboarding
The key question to ask yourself:
🤔 The Decision Question
"Is the number of items fixed and small, or dynamic and potentially large?"
flowchart TD
A["Need scrollable content?"] --> B{"How many items?"}
B -->|"< 20-30 items
OR
Fixed content"| C["Use ScrollView ✅"]
B -->|"30+ items
OR
Dynamic data from API"| D{"Grouped with headers?"}
D -->|"No"| E["Use FlatList ✅"]
D -->|"Yes"| F["Use SectionList ✅"]
C --> G["Examples:
• Settings page
• Form
• Product details"]
E --> H["Examples:
• Message list
• Feed
• Search results"]
F --> I["Examples:
• Contacts A-Z
• Grouped settings
• Category listing"]
style C fill:#c8e6c9
style E fill:#c8e6c9
style F fill:#c8e6c9
The Gray Zone
What about lists with 20-50 items? This is the "gray zone" where either approach might work. Here's how to decide:
- Items are simple (text only): ScrollView might be fine up to 50 items
- Items have images: Use FlatList at 20+ items
- Items have complex layouts: Use FlatList at 15+ items
- List can grow: Always use FlatList (future-proof)
- Targeting older devices: Use FlatList at lower thresholds
⚠️ When in Doubt, Use FlatList
FlatList works perfectly fine for small lists too—it just renders all items since they fit in the viewport. There's minimal overhead for using FlatList with 10 items, but significant pain if your ScrollView grows to 100. Default to FlatList for any list of data from an external source.
Hands-On Exercises
Let's solidify your understanding of ScrollView limitations and the virtualization concept.
Exercise 1: Profile the Performance Cliff
Create a test app that demonstrates the ScrollView performance cliff.
Requirements:
- Create a component that renders N items in a ScrollView
- Each item should have: an image placeholder (colored View), title, description, and timestamp
- Add buttons to test with 10, 50, 100, 500, and 1000 items
- Display the render time for each test
💡 Hint
Use performance.now() before setting state and requestAnimationFrame after render to measure time. Generate items with Array.from().
✅ Solution
import React, { useState, useCallback } from 'react';
import {
ScrollView,
View,
Text,
Pressable,
StyleSheet,
} from 'react-native';
interface Item {
id: number;
title: string;
description: string;
timestamp: string;
color: string;
}
const generateItems = (count: number): Item[] => {
const colors = ['#e91e63', '#9c27b0', '#3f51b5', '#009688', '#ff9800'];
return Array.from({ length: count }, (_, i) => ({
id: i,
title: `Item ${i + 1}`,
description: `This is a detailed description for item number ${i + 1}. It contains enough text to simulate real content.`,
timestamp: new Date(Date.now() - i * 60000).toLocaleString(),
color: colors[i % colors.length],
}));
};
export default function PerformanceTest() {
const [items, setItems] = useState<Item[]>([]);
const [renderTime, setRenderTime] = useState<number | null>(null);
const [isLoading, setIsLoading] = useState(false);
const runTest = useCallback((count: number) => {
setIsLoading(true);
setRenderTime(null);
const start = performance.now();
// Clear first, then set new items
setItems([]);
setTimeout(() => {
setItems(generateItems(count));
// Measure after render completes
requestAnimationFrame(() => {
requestAnimationFrame(() => {
const end = performance.now();
setRenderTime(end - start);
setIsLoading(false);
});
});
}, 50);
}, []);
return (
<View style={styles.container}>
{/* Test Controls */}
<View style={styles.controls}>
<Text style={styles.title}>ScrollView Performance Test</Text>
<View style={styles.buttonRow}>
{[10, 50, 100, 500, 1000].map(count => (
<Pressable
key={count}
style={[styles.button, isLoading && styles.buttonDisabled]}
onPress={() => runTest(count)}
disabled={isLoading}
>
<Text style={styles.buttonText}>{count}</Text>
</Pressable>
))}
</View>
{renderTime !== null && (
<View style={[
styles.result,
renderTime < 100 ? styles.resultGood :
renderTime < 500 ? styles.resultWarn :
styles.resultBad
]}>
<Text style={styles.resultText}>
Rendered {items.length} items in {renderTime.toFixed(0)}ms
</Text>
</View>
)}
</View>
{/* The ScrollView being tested */}
<ScrollView style={styles.list}>
{items.map(item => (
<View key={item.id} style={styles.item}>
<View style={[styles.avatar, { backgroundColor: item.color }]} />
<View style={styles.content}>
<Text style={styles.itemTitle}>{item.title}</Text>
<Text style={styles.itemDesc}>{item.description}</Text>
<Text style={styles.itemTime}>{item.timestamp}</Text>
</View>
</View>
))}
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
controls: {
padding: 16,
backgroundColor: 'white',
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
title: {
fontSize: 18,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 12,
},
buttonRow: {
flexDirection: 'row',
justifyContent: 'space-around',
},
button: {
backgroundColor: '#2196F3',
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 4,
},
buttonDisabled: {
backgroundColor: '#bdbdbd',
},
buttonText: {
color: 'white',
fontWeight: 'bold',
},
result: {
marginTop: 12,
padding: 12,
borderRadius: 4,
alignItems: 'center',
},
resultGood: {
backgroundColor: '#c8e6c9',
},
resultWarn: {
backgroundColor: '#fff3cd',
},
resultBad: {
backgroundColor: '#ffcdd2',
},
resultText: {
fontWeight: 'bold',
},
list: {
flex: 1,
},
item: {
flexDirection: 'row',
padding: 12,
backgroundColor: 'white',
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
avatar: {
width: 50,
height: 50,
borderRadius: 25,
marginRight: 12,
},
content: {
flex: 1,
},
itemTitle: {
fontSize: 16,
fontWeight: 'bold',
},
itemDesc: {
fontSize: 14,
color: '#666',
marginTop: 4,
},
itemTime: {
fontSize: 12,
color: '#999',
marginTop: 4,
},
});
Exercise 2: Memory Impact Analysis
Analyze how different item complexities affect memory usage.
Requirements:
- Create three different item components: Simple (text only), Medium (text + styled container), Complex (text + image + multiple styled views)
- Render 200 items of each type in separate ScrollViews
- Use React Native's performance monitor to observe memory differences
- Document your findings
💡 Hint
Enable the Performance Monitor from the React Native dev menu (shake device or Cmd+D in simulator). Compare memory usage between the three item types. Note how images dramatically increase memory.
✅ Solution
import React, { useState } from 'react';
import {
ScrollView,
View,
Text,
Image,
Pressable,
StyleSheet,
} from 'react-native';
type ItemType = 'simple' | 'medium' | 'complex';
// Simple: Just text
const SimpleItem = ({ index }: { index: number }) => (
<Text style={styles.simpleText}>Item {index + 1}</Text>
);
// Medium: Text with styled container
const MediumItem = ({ index }: { index: number }) => (
<View style={styles.mediumContainer}>
<Text style={styles.mediumTitle}>Item {index + 1}</Text>
<Text style={styles.mediumSubtitle}>
This is a medium complexity item with some description text
</Text>
</View>
);
// Complex: Full card with image, multiple text elements, buttons
const ComplexItem = ({ index }: { index: number }) => (
<View style={styles.complexContainer}>
<Image
source={{
uri: `https://picsum.photos/seed/${index}/100/100`
}}
style={styles.complexImage}
/>
<View style={styles.complexContent}>
<Text style={styles.complexTitle}>Item {index + 1}</Text>
<Text style={styles.complexSubtitle}>
Complex item with image and multiple text elements
</Text>
<View style={styles.complexMeta}>
<Text style={styles.complexMetaText}>12 likes</Text>
<Text style={styles.complexMetaText}>3 comments</Text>
<Text style={styles.complexMetaText}>Share</Text>
</View>
</View>
</View>
);
export default function MemoryAnalysis() {
const [itemType, setItemType] = useState<ItemType>('simple');
const items = Array.from({ length: 200 }, (_, i) => i);
const renderItem = (index: number) => {
switch (itemType) {
case 'simple':
return <SimpleItem key={index} index={index} />;
case 'medium':
return <MediumItem key={index} index={index} />;
case 'complex':
return <ComplexItem key={index} index={index} />;
}
};
return (
<View style={styles.container}>
<View style={styles.controls}>
<Text style={styles.title}>Memory Impact Analysis</Text>
<Text style={styles.subtitle}>
Open Performance Monitor to compare memory usage
</Text>
<View style={styles.buttonRow}>
{(['simple', 'medium', 'complex'] as ItemType[]).map(type => (
<Pressable
key={type}
style={[
styles.button,
itemType === type && styles.buttonActive,
]}
onPress={() => setItemType(type)}
>
<Text style={[
styles.buttonText,
itemType === type && styles.buttonTextActive,
]}>
{type.charAt(0).toUpperCase() + type.slice(1)}
</Text>
</Pressable>
))}
</View>
<Text style={styles.info}>
Rendering 200 {itemType} items
</Text>
</View>
<ScrollView style={styles.list}>
{items.map(renderItem)}
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
controls: {
padding: 16,
backgroundColor: 'white',
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
title: {
fontSize: 18,
fontWeight: 'bold',
textAlign: 'center',
},
subtitle: {
fontSize: 12,
color: '#666',
textAlign: 'center',
marginTop: 4,
},
buttonRow: {
flexDirection: 'row',
justifyContent: 'space-around',
marginTop: 16,
},
button: {
paddingHorizontal: 20,
paddingVertical: 10,
borderRadius: 20,
backgroundColor: '#e0e0e0',
},
buttonActive: {
backgroundColor: '#2196F3',
},
buttonText: {
fontWeight: '500',
color: '#666',
},
buttonTextActive: {
color: 'white',
},
info: {
textAlign: 'center',
marginTop: 12,
color: '#666',
},
list: {
flex: 1,
},
// Simple item styles
simpleText: {
padding: 12,
fontSize: 14,
borderBottomWidth: 1,
borderBottomColor: '#eee',
backgroundColor: 'white',
},
// Medium item styles
mediumContainer: {
padding: 16,
backgroundColor: 'white',
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
mediumTitle: {
fontSize: 16,
fontWeight: 'bold',
},
mediumSubtitle: {
fontSize: 14,
color: '#666',
marginTop: 4,
},
// Complex item styles
complexContainer: {
flexDirection: 'row',
padding: 12,
backgroundColor: 'white',
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
complexImage: {
width: 80,
height: 80,
borderRadius: 8,
backgroundColor: '#e0e0e0',
},
complexContent: {
flex: 1,
marginLeft: 12,
},
complexTitle: {
fontSize: 16,
fontWeight: 'bold',
},
complexSubtitle: {
fontSize: 14,
color: '#666',
marginTop: 4,
},
complexMeta: {
flexDirection: 'row',
marginTop: 8,
gap: 16,
},
complexMetaText: {
fontSize: 12,
color: '#2196F3',
},
});
/*
Expected findings:
- Simple: ~10-15 MB
- Medium: ~20-30 MB
- Complex: ~80-150 MB (images are expensive!)
Note: Actual values vary by device and RN version.
The key insight is how dramatically images increase memory.
*/
Exercise 3: Decision Practice
For each scenario, decide whether to use ScrollView or FlatList (or SectionList) and explain why.
Scenarios:
- A settings page with 15 toggle options grouped into 3 categories
- A social media feed that loads posts from an API, with infinite scroll
- A checkout flow with shipping address, payment info, and order summary
- A contacts list that could have 5 contacts or 500 contacts
- A product details page with images, description, specs table, and reviews preview (showing 3 reviews with a "see all" button)
- A chat message history that could grow to thousands of messages
- A music app's "Now Playing" queue showing the next 20 songs
- An email inbox with categories (Primary, Social, Promotions)
✅ Answers
1. Settings page (15 toggles, 3 categories):
→ ScrollView - Small, fixed number of items. Categories are static.
2. Social media feed:
→ FlatList - Dynamic data from API, infinite scroll, potentially thousands of posts.
3. Checkout flow:
→ ScrollView - Fixed form content, known sections, not a list of data.
4. Contacts list (5-500):
→ FlatList - Could grow large, and even 500 contacts is beyond ScrollView's comfort zone. FlatList handles small lists fine anyway.
5. Product details:
→ ScrollView - Fixed structure, not rendering a list of dynamic data. The 3 preview reviews are fixed, not the full review list.
6. Chat messages:
→ FlatList (inverted) - Definitely needs virtualization for thousands of messages. Use inverted prop for chat UI.
7. Now Playing queue (20 songs):
→ FlatList or ScrollView - Gray zone. If it's always exactly 20, ScrollView is fine. If the queue can grow, use FlatList. When in doubt, FlatList.
8. Email inbox with categories:
→ SectionList - Grouped data (Primary, Social, Promotions), potentially large lists within each section.
Summary
In this lesson, you've learned one of the most important performance concepts in React Native development: why ScrollView doesn't scale for long lists and how virtualization solves this problem.
🎯 Key Takeaways
- ScrollView renders everything — All children are rendered immediately, regardless of visibility
- The performance cliff is real — Apps work fine with small lists, then suddenly fail with larger ones
- Memory is the silent killer — High memory usage causes crashes without warning
- Virtualization renders only what's visible — Plus a small buffer for smooth scrolling
- FlatList implements virtualization — It's the go-to solution for dynamic lists in React Native
- ScrollView is still useful — For forms, settings pages, and fixed content with under ~30 items
- When in doubt, use FlatList — It handles small lists fine and scales to any size
🔑 The Golden Rule
If your list data comes from an API or could grow beyond ~30 items, use FlatList.
Reserve ScrollView for fixed, known content like forms and settings.
What's Next?
Now that you understand why virtualization matters, it's time to learn how to use it effectively. In the next lesson, we'll dive deep into FlatList fundamentals—the API, required props, common patterns, and how to avoid the pitfalls that trip up many developers.