Skip to main content

React Native FlatList Optimization: Smooth Scrolling for Large Lists

· 6 min read
Mobile Developer
Last updated on May 18, 2026

React Native FlatList optimization

FlatList is fast when each row is cheap, keys are stable, virtualization is tuned for the screen, and the data layer avoids unnecessary re-renders. It becomes slow when rows contain heavy images, anonymous render functions, unstable item references, eager API calls, and too many mounted views. This guide focuses on practical fixes you can apply to production feeds, chats, catalogs, and marketplace screens.

Quick Answer

To optimize a React Native FlatList, use stable keys, memoize row components, keep renderItem stable, provide getItemLayout for fixed-height rows, tune initialNumToRender, maxToRenderPerBatch, and windowSize, paginate data, use thumbnails in rows, and profile on real devices. If the list is still too complex, evaluate a recycler-style list such as FlashList after measuring the current bottleneck.

For app-wide performance work, pair this with the React Native FlatList docs, the React Native performance guide, and our guide to improving performance on older Android devices.


Start With the Row, Not the Props

Virtualization props help, but they cannot rescue an expensive row. First make each row predictable:

  • use a stable id for keyExtractor;
  • keep row props small and serializable;
  • avoid inline objects and functions passed into every row;
  • move expensive formatting outside render where possible;
  • show thumbnails instead of original-size images;
  • avoid nested lists inside rows unless the product truly needs them.
import { memo } from 'react';
import { Image, Text, View } from 'react-native';

type ProductRowProps = {
title: string;
thumbnailUrl: string;
priceLabel: string;
};

export const ProductRow = memo(function ProductRow({
title,
thumbnailUrl,
priceLabel,
}: ProductRowProps) {
return (
<View style={{ height: 88, flexDirection: 'row', alignItems: 'center' }}>
<Image
source={{ uri: thumbnailUrl }}
style={{ width: 64, height: 64, borderRadius: 8 }}
/>
<View style={{ flex: 1, marginLeft: 12 }}>
<Text numberOfLines={1}>{title}</Text>
<Text>{priceLabel}</Text>
</View>
</View>
);
});

Keep renderItem Stable

If renderItem changes every render, the list has more work to do. Use useCallback and avoid closing over large changing objects.

import { useCallback } from 'react';
import { FlatList, ListRenderItemInfo } from 'react-native';
import { ProductRow } from './ProductRow';

type Product = {
id: string;
title: string;
thumbnailUrl: string;
priceLabel: string;
};

export function ProductList({ products }: { products: Product[] }) {
const renderItem = useCallback(({ item }: ListRenderItemInfo<Product>) => {
return (
<ProductRow
title={item.title}
thumbnailUrl={item.thumbnailUrl}
priceLabel={item.priceLabel}
/>
);
}, []);

return (
<FlatList
data={products}
keyExtractor={item => item.id}
renderItem={renderItem}
/>
);
}

Only pass extraData when the list really depends on external state. If one selected item changes, prefer row-level state or a compact selected ID instead of passing a large object that changes constantly.


Mega Bundle Sale is ON! Get ALL of our React Native codebases at 90% OFF discount 🔥

Get the Mega Bundle

Tune Virtualization Props

The best values depend on row height, device class, image weight, and scroll speed. Start conservative, then measure.

<FlatList
data={products}
keyExtractor={item => item.id}
renderItem={renderItem}
initialNumToRender={8}
maxToRenderPerBatch={8}
updateCellsBatchingPeriod={50}
windowSize={5}
removeClippedSubviews
/>

What each prop changes:

  • initialNumToRender: rows rendered for the first screen. Enough to fill the viewport, not the whole feed.
  • maxToRenderPerBatch: rows rendered per batch while scrolling. Lower values reduce JS work but can reveal blanks.
  • updateCellsBatchingPeriod: time between rendering batches.
  • windowSize: number of viewport-height windows kept mounted around the visible area.
  • removeClippedSubviews: detaches offscreen native views. Test carefully on complex layouts.

The React Native docs explain the official behavior and caveats for these props.

Add getItemLayout for Fixed-Height Rows

When every row has the same height, getItemLayout lets FlatList skip runtime measurement.

const ROW_HEIGHT = 88;

<FlatList
data={products}
keyExtractor={item => item.id}
renderItem={renderItem}
getItemLayout={(_, index) => ({
length: ROW_HEIGHT,
offset: ROW_HEIGHT * index,
index,
})}
/>

Include separator height in the offset if your list uses ItemSeparatorComponent. Do not use this optimization for rows with unpredictable heights.

Paginate and Cancel Requests

A fast list can still lag if the data layer pushes too much work into it.

Use these rules:

  • request one page at a time;
  • avoid replacing the entire array when appending one page;
  • cancel stale requests when a screen unmounts;
  • debounce search;
  • keep loading and error states outside expensive rows;
  • avoid refetching every time the screen regains focus unless the data is truly stale.
async function loadPage(cursor?: string, signal?: AbortSignal) {
const url = new URL('https://api.example.com/products');

if (cursor) {
url.searchParams.set('cursor', cursor);
}

const response = await fetch(url.toString(), { signal });

if (!response.ok) {
throw new Error('Failed to load products');
}

return response.json();
}

For networking details, see React Native REST API integration and debugging network requests.

Use Images Deliberately

Images dominate memory and scrolling cost in many production lists.

Prefer:

  • server-generated thumbnails;
  • fixed row image dimensions;
  • progressive loading only when the UX needs it;
  • cache policies that match the product;
  • detail-screen full images instead of feed-row originals.

Profile after deep scrolling, not only on the first screen. Memory issues often appear after the user has loaded several pages.

When to Consider FlashList

If a list still drops frames after row cleanup, pagination, image resizing, and FlatList tuning, evaluate FlashList. It is designed for high-performance React Native lists and has a similar API, but it is still a dependency choice. Test it against your actual screen, target devices, and architecture.

Do not switch list libraries to hide an expensive row. Fix the row first, then compare.

Production Checklist

Before shipping a list-heavy screen:

  • profile on at least one lower-end Android device;
  • verify stable keyExtractor values;
  • memoize row components and keep renderItem stable;
  • add getItemLayout only for fixed-height rows;
  • tune virtualization props from measurements;
  • use thumbnails and explicit image dimensions;
  • paginate and cancel stale requests;
  • test pull-to-refresh, empty, loading, error, and offline states;
  • run the React Native release checklist.
Looking for a custom mobile application?

Our team of expert mobile developers can help you build a custom mobile app that meets your specific needs.

Get in Touch