React Native Lists - Treat Them as a Performance Subsystem, Not a Component

In many React Native codebases, list performance is treated as a local concern; a screen feels slow, so someone tweaks FlatList props until it’s “good enough”. initialNumToRender, windowSize, maybe getItemLayout if someone is feeling ambitious. Then the team moves on.

This approach works, until it doesn’t.

Once your app grows, lists stop being just UI components. They become performance-critical infrastructure. Feeds, chats, search results, product catalogs, lists dominate runtime, memory usage, and perceived speed. At that point, tuning FlatList props is table stakes. The real gains come from treating lists as a subsystem with explicit rules, shared abstractions, and enforcement.

I decided to write this article to argue for that shift and shows what developers can do today to improve an existing React Native project.


The Ceiling of “Just Tune FlatList”

Most experienced teams know the basics:

<FlatList
  data={data}
  keyExtractor={item => item.id}
  initialNumToRender={10}
  maxToRenderPerBatch={5}
  windowSize={5}
  removeClippedSubviews
/>

Sometimes they add:

getItemLayout={(_, index) => ({
  length: ITEM_HEIGHT,
  offset: ITEM_HEIGHT * index,
  index,
})}

This is good practice, but it assumes something rarely true in real apps:

Once those assumptions break, FlatList tuning alone hits a ceiling. Scroll hitching returns. Memory spikes. Skeletons flash unpredictably. Developers start “fixing” individual screens instead of addressing the root cause.

That’s the signal you need a system.


Lists as a Performance Subsystem

A subsystem has three properties:

  1. Explicit constraints
  2. Shared primitives
  3. Enforcement mechanisms

For lists, this means defining how items are measured, prefetched, rendered, and composed—and making those decisions reusable and reviewable.

Let’s break that down.


1. Item Measurement Is a First-Class Concern

Most teams rely on runtime measurement (onLayout) without realizing the cost. Measuring during scroll is expensive and unpredictable, especially with heterogeneous items.

An advanced approach is to design item measurement up front.

Strategy A: Fixed-height contracts

If an item can be fixed height, enforce it.

export const FEED_ITEM_HEIGHT = 96;

export function FeedItem({item}: Props) {
  return (
    <View style=>
      {/* content */}
    </View>
  );
}

This unlocks cheap, deterministic getItemLayout and eliminates runtime measurement entirely.

Strategy B: Height classes

When fixed height isn’t realistic, define height classes instead of infinite variability.

type ItemHeightClass = 'compact' | 'regular' | 'expanded';

const HEIGHTS: Record<ItemHeightClass, number> = {
  compact: 64,
  regular: 96,
  expanded: 140,
};

Each item declares its class in data:

{
  id: '123',
  heightClass: 'regular',
}

Now getItemLayout is still computable, and layout variance stays bounded.

Actionable rule: No list item ships without an explicit height strategy.


2. Prefetching Is Part of Rendering, Not Networking

Many teams prefetch data globally and hope lists benefit indirectly. That’s backwards. Lists know exactly what the user will see next.

A list subsystem should own prefetching semantics.

Viewport-aware prefetch

const onViewableItemsChanged = useRef(({viewableItems}) => {
  const lastVisible = viewableItems[viewableItems.length - 1];
  if (!lastVisible) return;

  const index = lastVisible.index ?? 0;

  prefetchItems(index + 1, index + 10);
}).current;

<FlatList
  onViewableItemsChanged={onViewableItemsChanged}
  viewabilityConfig=
/>

This shifts prefetching from “when data loads” to “when the user scrolls”.

Asset prefetching

Images, videos, and fonts should follow the same contract:

function prefetchItemAssets(item: FeedItem) {
  Image.prefetch(item.thumbnailUrl);
}

Tie this into the same viewport logic.

Actionable rule: If an item can’t render instantly, it must be prefetched before it enters the viewport.


3. Placeholder Strategy Must Be Deterministic

Skeletons are often implemented ad hoc: a shimmer here, a spinner there. The result is visual noise and layout shifts.

Instead, placeholders should be structurally identical to real cells.

Bad placeholder

Good placeholder

function FeedItemPlaceholder() {
  return (
    <View style=>
      <Skeleton width={48} height={48} />
      <Skeleton width="60%" />
    </View>
  );
}

function FeedItemCell({ item }: Props) {
  if (!item.isLoaded) {
    return <FeedItemPlaceholder />;
  }

  return <FeedItem item={item} />;
}

The list doesn’t care whether data is loaded. Layout stays stable. Scroll performance improves because React Native doesn’t need to reflow items mid-scroll.

Actionable rule: Placeholders must match final layout dimensions exactly.


4. Cell Composition Rules Prevent Accidental Slowness

One of the most common performance regressions comes from “harmless” changes inside list items:

A list subsystem defines what is allowed inside a cell.

Cell boundary rule

Allowed

Forbidden

Enforce this with structure:

function FeedItemCell(props: Props) {
  return (
    <CellBoundary>
      <FeedItemView {...props} />
    </CellBoundary>
  );
}

const FeedItemView = React.memo(function FeedItemView(props: Props) {
  // rendering only
});

Now performance assumptions are explicit and reviewable.

Actionable rule: Treat list cells like render() in a game engine: no side effects, no surprises.


5. Make the System Visible in Code Review

None of this matters if it lives in someone’s head.

Strong teams encode list rules into the project itself:

Example:

export const FeedListConfig = {
  itemHeight: FEED_ITEM_HEIGHT,
  prefetchAhead: 10,
  placeholder: FeedItemPlaceholder,
};

Now list behavior is discoverable and consistent across screens.


Conclusion: Performance Is a Design Problem

FlatList tuning is necessary, but it’s not sufficient. Real-world React Native apps demand a more disciplined approach.

When you treat lists as a performance subsystem, you:

The key insight is simple but uncomfortable: most list performance issues are self-inflicted at the architecture level, not the prop level.

Design the system. Enforce the rules. Then let FlatList do what it does best.