Skip to main content

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.

0 20 40 60 FPS 10 50 100 500 1000 5000 Number of Items 60 FPS target The Cliff! ScrollView FlatList (preview)

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.

Memory Usage: ScrollView vs Virtualized List ScrollView 1000 items = ~40MB ALL ITEMS IN MEMORY Virtualized List 1000 items = ~0.5MB VISIBLE ONLY ❌ ScrollView Impact • High memory pressure • Risk of OS termination • Slow initial render • Battery drain ✅ Virtualized Benefits • Constant memory usage • Stable performance • Fast initial render • Scales infinitely

🚨 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 Your Data (1000 items) Item 1 Item 2 Item 3 Item 4 Item 5 Item 6 ... Item 247 Item 248 Item 249 Item 250 Item 251 Item 252 Item 253 ... Item 998 Item 999 Item 1000 Device Screen (visible area) Item 247 Item 248 Item 249 Item 250 Item 251 Item 252 Item 253 Memory (what's actually rendered) Buffer: Item 247 Buffer: Item 248 Visible: 249 Visible: 250 Visible: 251 Visible: 252 Buffer: Item 253 Legend Visible items Buffer zone Not rendered Performance Impact ScrollView: 1000 items rendered Virtualized: 7 items rendered 99.3% reduction in render work!

The rendering window consists of three zones:

  1. Visible Zone: Items currently on screen. These must be rendered.
  2. Buffer Zone: A few items above and below the visible area, pre-rendered for smooth scrolling.
  3. 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:

  1. A settings page with 15 toggle options grouped into 3 categories
  2. A social media feed that loads posts from an API, with infinite scroll
  3. A checkout flow with shipping address, payment info, and order summary
  4. A contacts list that could have 5 contacts or 500 contacts
  5. A product details page with images, description, specs table, and reviews preview (showing 3 reviews with a "see all" button)
  6. A chat message history that could grow to thousands of messages
  7. A music app's "Now Playing" queue showing the next 20 songs
  8. 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.