RoadmapDay 37 / 80
React NativeMonth 2 · Week 8

Day 37: Mobile Performance: FlatList vs FlashList & Virtualization Tuning

Understand list virtualization deeply enough to correctly tune FlatList/FlashList props instead of cargo-culting them.

Mark this day complete

Study

Concepts

Virtualization: only render what is (nearly) on screen

Rendering 10,000 rows as real native views would exhaust memory and destroy scroll performance. Virtualization renders only the rows currently visible plus a small buffer ("window") above/below, recycling views as the user scrolls — rows that scroll far off-screen are unmounted (or recycled) rather than kept alive. FlatList (RN core) implements this via `VirtualizedList` underneath, tuned with props like `windowSize` (how many screens' worth of content to keep rendered around the viewport), `initialNumToRender`, `maxToRenderPerBatch`, and `removeClippedSubviews`.

The classic FlatList performance killer is `getItemLayout` being omitted for variable-height rows — without it, FlatList must measure each row after render to know scroll offsets, causing layout thrashing on fast scrolls; providing `getItemLayout` (when row height is known/fixed) lets it compute offsets mathematically instead, a significant win.

FlashList: recycling instead of mount/unmount

FlashList (Shopify) uses a fundamentally different technique — cell RECYCLING, similar to native `UICollectionView`/`RecyclerView`: instead of unmounting a row that scrolls off and mounting a brand-new one that scrolls in, it reuses the SAME underlying view instance and just updates its content, avoiding the (often expensive) mount/unmount lifecycle entirely on every scroll step. This is why FlashList asks for an `estimatedItemSize` up front — it needs a size estimate to plan recycling, whereas FlatList measures more reactively.

FlashList is close to a drop-in replacement for FlatList's API for most use cases, and is the current recommendation for RN lists with more than a few hundred items or with complex row components, where FlatList's mount/unmount churn becomes the visible bottleneck.

See It

Visualizations

Visualization

Virtualization window while scrolling a 10,000-row list

Only rows inside the window are mounted; the rest exist only as data.

rows 1-50 (off-screen, unmounted)
rows 51-60 (visible + buffer, mounted)
rows 61-10000 (off-screen, unmounted)
recycled/unmounted
RENDERED
not yet rendered

Build It

Code Examples

A well-tuned FlatList for a fixed-height row

jsx
const ROW_HEIGHT = 72;

function ContactList({ contacts }) {
  return (
    <FlatList
      data={contacts}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => <ContactRow contact={item} />}
      // Fixed row height -> compute offsets mathematically, no measuring pass
      getItemLayout={(data, index) => ({
        length: ROW_HEIGHT,
        offset: ROW_HEIGHT * index,
        index,
      })}
      initialNumToRender={12}   // enough to fill the first screen
      windowSize={7}            // ~7 screens worth kept around the viewport
      removeClippedSubviews     // let Android reclaim off-screen native views
      maxToRenderPerBatch={10}
    />
  );
}

The same list with FlashList — recycling by default

jsx
import { FlashList } from '@shopify/flash-list';

function ContactList({ contacts }) {
  return (
    <FlashList
      data={contacts}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => <ContactRow contact={item} />}
      estimatedItemSize={72} // needed up front to plan cell recycling
    />
  );
}

Remember

Key Takeaways

  • Virtualization mounts only visible-plus-buffer rows — the core technique behind every large-list solution.
  • getItemLayout removes the need to measure rows for offset calculation — a major win when row height is known.
  • windowSize/initialNumToRender/maxToRenderPerBatch trade memory and initial render cost against scroll smoothness — tune, don't guess.
  • FlashList recycles view instances instead of mount/unmount per row, avoiding lifecycle churn on every scroll step.
  • estimatedItemSize is FlashList's equivalent up-front hint to getItemLayout — required for its recycling strategy to plan correctly.

Do It

Practice

  1. 1Render a 5,000-item FlatList without getItemLayout, profile scroll performance, then add it and compare.
  2. 2Swap the same list to FlashList with a correct estimatedItemSize and compare JS frame drops using the RN Perf Monitor.
  3. 3Experiment with windowSize values (3 vs 21) on a heavy-row list and describe the memory-vs-smoothness tradeoff you observe.