React Native FlatList Performance: A Complete Guide to Fast Scrolling Lists
React NativeFlatListPerformanceInfinite ScrollMobile UX

React Native FlatList Performance: A Complete Guide to Fast Scrolling Lists

NNative Dev Hub Editorial Team
2026-11-03
7 min read

Use this practical React Native FlatList checklist to improve scrolling, rendering, images, pagination, refresh states, and performance testing.

Fast scrolling in a React Native app depends on more than turning on virtualization. This guide provides a reusable FlatList performance checklist for feeds, search results, catalogs, and other long lists, with practical patterns for row rendering, pagination, images, refresh behavior, profiling, and regression testing.

Overview

FlatList is usually the right starting point for a large or growing collection because it renders only a window of items instead of mounting the entire dataset at once. That does not make every list fast automatically. A list can still stutter when each row is expensive, data changes too frequently, images are oversized, or pagination triggers repeated renders.

Begin with a simple mental model: every visible row consumes rendering time, layout work, memory, and often network or image-decoding resources. Performance work is therefore a process of reducing unnecessary work rather than applying one universal prop. Measure the list on representative devices, with realistic item content and data volume, before and after each meaningful change.

A dependable React Native FlatList implementation usually has these characteristics:

  • Stable keys and predictable item identity.
  • Rows that avoid rerendering when their relevant data has not changed.
  • Images sized for their display area and loaded with sensible placeholders.
  • Pagination that prevents duplicate requests and handles retries.
  • Explicit loading, empty, error, and refreshing states.
  • Performance settings tuned from measurements rather than copied blindly.

If the list is part of a navigational flow, keep screen-level concerns separate from row rendering. For routing patterns, see the React Native navigation guide. If authentication controls which feed a user can access, the Expo Router authentication guide provides the related structure.

Checklist by scenario

For a basic list

Start with a small, explicit component. Use a stable identifier from the data source rather than the array index whenever items can be inserted, removed, or reordered.

const renderItem = ({ item }: { item: Product }) => (
  <ProductRow product={item} />
);

<FlatList
  data={products}
  renderItem={renderItem}
  keyExtractor={(item) => item.id}
  contentContainerStyle={products.length === 0 ? styles.emptyList : undefined}
  ListEmptyComponent={<EmptyState />}
/>
  • Keep renderItem stable with a module-level function or useCallback when appropriate.
  • Use keyExtractor with a unique, stable value.
  • Move row logic into a focused component instead of placing a large render tree inline.
  • Keep list data normalized and avoid creating a new transformed array on every render unless the transformation is memoized.

For rows that update frequently

Wrap a row in React.memo when its props are stable and the component can safely skip unrelated updates. Memoization is not automatically beneficial: if the parent creates new object, array, or function props every render, the row may still render. Pass small, stable props where possible.

const ProductRow = React.memo(function ProductRow({
  id,
  title,
  selected,
  onPress,
}: ProductRowProps) {
  return (
    <Pressable onPress={() => onPress(id)}>
      <Text>{title}</Text>
      {selected ? <Text>Selected</Text> : null}
    </Pressable>
  );
});

Check callback dependencies carefully. A callback that changes because it captures an entire screen state object can invalidate every row. Prefer passing an identifier and a stable action where that matches the component design.

For fixed-height rows

If every item has the same known height, getItemLayout can avoid repeated measurement work and make scrolling to an index more predictable.

const ROW_HEIGHT = 72;

const getItemLayout = (_data: Product[] | null | undefined, index: number) => ({
  length: ROW_HEIGHT,
  offset: ROW_HEIGHT * index,
  index,
});

Only use this when the value matches the actual rendered layout, including separators or other fixed spacing. An incorrect layout calculation can cause jumps, clipped rows, or inaccurate scroll positions. For variable-height content, do not guess a single height simply to enable this optimization.

For infinite scroll and pagination

Use onEndReached as a signal to request the next page, not as proof that a request should always begin. Guard against duplicate calls, track whether another page exists, and distinguish initial loading from loading the next page.

const loadNextPage = async () => {
  if (loadingMore || !hasNextPage) return;

  setLoadingMore(true);
  try {
    const nextPage = await fetchProducts({ cursor: nextCursor });
    setProducts((current) => [...current, ...nextPage.items]);
    setNextCursor(nextPage.nextCursor);
    setHasNextPage(Boolean(nextPage.nextCursor));
  } finally {
    setLoadingMore(false);
  }
};

<FlatList
  data={products}
  onEndReached={loadNextPage}
  onEndReachedThreshold={0.5}
  ListFooterComponent={loadingMore ? <ActivityIndicator /> : null}
/>

The threshold is an experience setting, not a performance fix. Choose it based on request latency, row height, and how much content a user can consume before the next page arrives. Make the fetch path idempotent or otherwise protected against repeated calls.

For feeds with images

  • Request or select an image size close to the rendered dimensions.
  • Reserve the image area so rows do not repeatedly change height as images load.
  • Use placeholders for slow or missing images.
  • Test cached, uncached, slow-network, and error states.
  • Avoid mounting several large decorative images in every row.

Image behavior can dominate perceived list performance even when JavaScript rendering is efficient. Treat image loading as part of the list budget, not as a separate concern.

For pull-to-refresh, empty, and error states

Use refreshing and onRefresh for a visible refresh operation, but do not replace existing content with a full-screen spinner during every refresh. Preserve the current list when possible, show a footer indicator for pagination, and provide a retry action for recoverable errors.

Test the zero-item state separately from the failed-request state. An empty catalog, a network failure, and a filtered-out result may need different messages and actions. Keep empty-state layouts inside the list when you want them to participate in scrolling and safe-area behavior.

What to double-check

Windowing settings

Props such as initialNumToRender, maxToRenderPerBatch, updateCellsBatchingPeriod, and windowSize trade memory, fill rate, and responsiveness against one another. Increasing every value can make blank areas less likely but can also increase work during scrolling. Change one setting at a time and test on a slower representative device.

Data identity

When appending a page, preserve existing item references where their content has not changed. Rebuilding every object can make memoized rows appear changed. Also check selectors and state subscriptions: a screen subscribed to broad state may rerender the list for unrelated updates.

Interaction cost

Profile while flinging, tapping, refreshing, opening a menu, and receiving new data. A list that scrolls well in isolation may still feel slow when a bottom sheet, animated header, navigation transition, or keyboard is active. Review the broader React Native performance optimization guide when the bottleneck is not limited to the list.

Regression checks

Record a small benchmark before changing the implementation: device or simulator, build mode, item count, row type, image state, and the interaction tested. After a dependency or feature change, repeat the same scenario. A consistent test setup is more useful than an isolated impression from a development build.

Common mistakes

  • Using array indexes as keys: insertions and deletions can associate the wrong state with a row.
  • Putting expensive work in render: filtering, sorting, parsing, or formatting large values for every row increases scroll-time work. Precompute or memoize where appropriate.
  • Memoizing everything: shallow comparison has a cost, and unstable props defeat it. Confirm that memoization reduces actual renders.
  • Adding nested virtualized lists: a vertical FlatList inside another vertical virtualized list can create measurement and scrolling problems. Restructure the screen or use list headers and footers where suitable.
  • Ignoring duplicate pagination calls: rapid scrolling, short datasets, and rerenders can trigger the end callback more than once.
  • Optimizing only in development: development tooling can change timing. Compare release-like builds while still using debuggable profiling methods.
  • Rendering every control in every row: menus, complex animations, and hidden interactive elements still add mounting and layout cost. Render them only when needed.

Accessibility should remain part of the optimization work. Smaller text, reduced controls, or removed labels are not valid performance fixes. Check accessible names, focus behavior, touch targets, and dynamic content after changing a row. The React Native accessibility checklist is useful for this review.

When to revisit

Revisit this checklist before a major release, before a seasonal traffic or catalog change, and whenever a list gains a new row feature, image treatment, animation, filter, or pagination path. Also repeat the review after upgrading React Native, Expo, a navigation package, an image library, or a state-management dependency; rendering behavior and defaults can change even when the list code does not.

Use this practical sequence:

  1. Choose one representative list and record its current interaction baseline.
  2. Test initial load, rapid scrolling, refresh, pagination, empty results, errors, and image failures.
  3. Use profiling tools to identify whether the issue is rendering, layout, JavaScript work, images, network latency, or memory pressure.
  4. Apply one focused change, then repeat the same scenarios on representative devices.
  5. Keep the simplest implementation that meets the measured target, and document any tuned FlatList props.

FlatList performance is an ongoing engineering check rather than a one-time configuration task. When row complexity, data volume, or user interaction changes, return to the evidence: stable keys, controlled renders, accurate layout assumptions, bounded pagination, efficient images, and repeatable measurements.

Related Topics

#React Native#FlatList#Performance#Infinite Scroll#Mobile UX
N

Native Dev Hub Editorial Team

React Native Editors

Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.