Skip to main content

FlashList vs FlatList vs LegendList: React Native Lists 2026

·PkgPulse Team

FlashList vs FlatList vs LegendList: React Native Lists 2026

TL;DR

Rendering long lists in React Native is famously challenging — FlatList is the built-in solution but struggles with scroll jank and blank flashes for large datasets. FlatList (built into React Native) works for lists under a few hundred items but degrades significantly beyond that. FlashList (Shopify) is the production-tested replacement — uses a recycler pattern that reuses rendered components instead of destroying and recreating them, achieving 5-10x better performance for large datasets. LegendList is the newest entry — built on React Native's Fabric architecture and Reanimated, claiming to fix blank flashes and providing even smoother scrolling for very large lists. For most apps (< 500 items): FlatList. For production apps with large datasets (500+ items): FlashList. For apps hitting FlashList's limits or needing the smoothest possible experience: LegendList.

Key Takeaways

  • FlatList blanks at high scroll speed — unmounted items render as blank white space
  • FlashList reuses DOM components — no creation/destruction overhead; 5-10x faster than FlatList
  • LegendList is built on Fabric/JSI — new architecture only, but eliminates blank flashes
  • FlashList requires estimatedItemSize — must set an estimated item height/width
  • FlatList is zero setup — built into React Native, works for most use cases
  • FlashList GitHub stars: 4k — Shopify uses it in their production apps
  • All three support horizontal listshorizontal prop works across all

Why FlatList Struggles at Scale

React Native's FlatList uses a windowing approach:

List with 10,000 items:
  - Renders ~20 items in the visible viewport
  - Destroys components as they scroll off-screen
  - Creates new components as they scroll into view

Problem: Creation/destruction is expensive
  - JS creates new component instances
  - Bridge sends layout data to native
  - At high scroll speed: blank flash while new items render

FlashList's recycler approach:

FlashList recycler:
  - Keeps ~30 component instances in memory
  - When item scrolls off: recycle it for the next incoming item
  - Update props instead of recreating component
  - No blank flashes — always showing content

FlatList: The Built-In Standard

FlatList is React Native's built-in virtualized list. Zero setup, well-documented, and sufficient for most use cases.

Basic Usage

import { FlatList, View, Text, StyleSheet } from "react-native";

interface Post {
  id: string;
  title: string;
  author: string;
  publishedAt: string;
}

function PostItem({ item }: { item: Post }) {
  return (
    <View style={styles.item}>
      <Text style={styles.title}>{item.title}</Text>
      <Text style={styles.author}>{item.author}</Text>
      <Text style={styles.date}>{item.publishedAt}</Text>
    </View>
  );
}

function PostsList({ posts }: { posts: Post[] }) {
  return (
    <FlatList
      data={posts}
      renderItem={({ item }) => <PostItem item={item} />}
      keyExtractor={(item) => item.id}
      // Performance optimizations
      removeClippedSubviews={true}   // Unmount off-screen views
      maxToRenderPerBatch={10}        // Batch render 10 at a time
      windowSize={10}                  // Render 10 * viewport height
      initialNumToRender={15}          // First render: 15 items
      getItemLayout={(data, index) => ({  // Fixed height optimization
        length: 80,
        offset: 80 * index,
        index,
      })}
    />
  );
}

FlatList Optimization Tips

// 1. Memoize renderItem to prevent unnecessary re-renders
const renderItem = useCallback(
  ({ item }: { item: Post }) => <PostItem item={item} />,
  []
);

// 2. Memoize PostItem component
const PostItem = memo(({ item }: { item: Post }) => {
  return (
    <View style={styles.item}>
      <Text>{item.title}</Text>
    </View>
  );
});

// 3. Use getItemLayout for fixed-height items (much faster)
const getItemLayout = useCallback(
  (data: Post[] | null | undefined, index: number) => ({
    length: 80,       // Fixed item height
    offset: 80 * index,
    index,
  }),
  []
);

// 4. Use keyExtractor returning stable IDs
const keyExtractor = useCallback((item: Post) => item.id, []);

function OptimizedList({ posts }: { posts: Post[] }) {
  return (
    <FlatList
      data={posts}
      renderItem={renderItem}
      keyExtractor={keyExtractor}
      getItemLayout={getItemLayout}
      removeClippedSubviews={true}
      maxToRenderPerBatch={20}
      windowSize={21}
    />
  );
}

Pull-to-Refresh and Infinite Scroll

function InfinitePostsList() {
  const [posts, setPosts] = useState<Post[]>([]);
  const [loading, setLoading] = useState(false);
  const [refreshing, setRefreshing] = useState(false);
  const [page, setPage] = useState(1);

  const loadMore = async () => {
    if (loading) return;
    setLoading(true);
    const newPosts = await fetchPosts(page + 1);
    setPosts((prev) => [...prev, ...newPosts]);
    setPage((p) => p + 1);
    setLoading(false);
  };

  const refresh = async () => {
    setRefreshing(true);
    const freshPosts = await fetchPosts(1);
    setPosts(freshPosts);
    setPage(1);
    setRefreshing(false);
  };

  return (
    <FlatList
      data={posts}
      renderItem={({ item }) => <PostItem item={item} />}
      keyExtractor={(item) => item.id}
      onEndReached={loadMore}
      onEndReachedThreshold={0.3}
      refreshing={refreshing}
      onRefresh={refresh}
      ListFooterComponent={loading ? <ActivityIndicator /> : null}
    />
  );
}

FlashList: Production-Grade Performance

FlashList from Shopify uses a recycler algorithm that achieves near-native scroll performance for large lists.

Installation

npx expo install @shopify/flash-list

Basic Usage (Drop-in for FlatList)

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

function PostsList({ posts }: { posts: Post[] }) {
  return (
    <FlashList
      data={posts}
      renderItem={({ item }) => <PostItem item={item} />}
      keyExtractor={(item) => item.id}
      estimatedItemSize={80}  // Required — FlashList uses this for layout estimation
    />
  );
}

FlashList with Variable Height Items

// FlashList handles variable-height items well
// Just provide estimatedItemSize as average height

function MessagesList({ messages }: { messages: Message[] }) {
  return (
    <FlashList
      data={messages}
      renderItem={({ item }) => (
        <MessageItem
          text={item.text}
          userId={item.userId}
          timestamp={item.timestamp}
        />
      )}
      estimatedItemSize={60}  // Average message height
      keyExtractor={(item) => item.id}
      inverted  // Show newest messages at bottom (chat-style)
      onEndReached={loadOlderMessages}
      onEndReachedThreshold={0.3}
    />
  );
}

FlashList Performance Tuning

// Advanced FlashList configuration for maximum performance
function OptimizedFlashList({ data }: { data: Item[] }) {
  return (
    <FlashList
      data={data}
      renderItem={({ item }) => <ItemComponent item={item} />}
      estimatedItemSize={100}

      // Override recycling type per item (for mixed layouts)
      getItemType={(item) => {
        if (item.type === "header") return "header";
        if (item.type === "ad") return "ad";
        return "default";
      }}

      // Pre-render invisible items for smoother scrolling
      drawDistance={300}  // Pixels above/below viewport to pre-render

      // Override estimated list size for better initial scroll
      estimatedListSize={{ height: 2000, width: 400 }}

      // Diagnostic: shows which items are recycled vs new
      // (development only)
      // disableAutoLayout={false}
    />
  );
}

Migrating from FlatList to FlashList

// Before (FlatList)
import { FlatList } from "react-native";

<FlatList
  data={data}
  renderItem={renderItem}
  keyExtractor={keyExtractor}
  removeClippedSubviews={true}
  maxToRenderPerBatch={10}
/>

// After (FlashList) — add estimatedItemSize, remove FlatList-specific props
import { FlashList } from "@shopify/flash-list";

<FlashList
  data={data}
  renderItem={renderItem}
  keyExtractor={keyExtractor}
  estimatedItemSize={80}  // Add this
  // Remove: removeClippedSubviews, maxToRenderPerBatchFlashList handles these internally
/>

LegendList: New Architecture Performance

LegendList is built for React Native's new architecture (Fabric + JSI). It aims to eliminate blank flashes entirely through synchronized rendering.

Installation

npm install @legendapp/list
# Requires React Native New Architecture (Fabric)

Basic Usage

import { LegendList } from "@legendapp/list";

function PostsList({ posts }: { posts: Post[] }) {
  return (
    <LegendList
      data={posts}
      renderItem={({ item }) => <PostItem item={item} />}
      keyExtractor={(item) => item.id}
      estimatedItemSize={80}
      // LegendList-specific: recycles with Reanimated for buttery smooth scrolling
      recycleItems
    />
  );
}

LegendList with Dynamic Heights

// LegendList handles dynamic heights better than FlashList
// No need to specify item type for mixed layouts

function DynamicList({ items }: { items: FeedItem[] }) {
  return (
    <LegendList
      data={items}
      renderItem={({ item }) => {
        if (item.type === "image") return <ImageCard item={item} />;
        if (item.type === "text") return <TextCard item={item} />;
        return <DefaultCard item={item} />;
      }}
      estimatedItemSize={120}
      recycleItems
      maintainScrollAtEnd  // Keeps scroll position when new items added at bottom
    />
  );
}

Performance Benchmark

Based on community benchmarks (scrolling 10,000 item list at maximum speed):

MetricFlatListFlashListLegendList
Blank flashesFrequentRareNone*
Memory usageHighLowLow
Initial render timeMediumFastFast
60fps scrollOften dropsMostly yesYes (Fabric)
Large dataset (10k)Slow✅ Fast✅ Fast
Setup effortZeroestimatedItemSizeFabric required
Mixed item typesManualgetItemTypeAuto

*LegendList requires New Architecture (Fabric)


Feature Comparison

FeatureFlatListFlashListLegendList
Built-in
New Architecture required
Recycler pattern
Variable heights
estimatedItemSizeRequiredRequired
Horizontal
SectionList equivSectionList✅ sectionsIn progress
Pull-to-refresh
GitHub starsBuilt-in4.4k1.5k
Production usage✅ Universal✅ ShopifyGrowing

When to Use Each

Choose FlatList if:

  • Your list has under 200-300 items and performance is acceptable
  • You need zero additional dependencies
  • You're working with SectionList (grouped items) — use React Native's SectionList
  • Compatibility with older React Native versions matters

Choose FlashList if:

  • Your list has 500+ items or performance is visibly poor with FlatList
  • You're on the old React Native architecture (not yet on Fabric)
  • You're building a production app (Shopify uses this at scale)
  • You need a reliable drop-in FlatList replacement with minimal API changes

Choose LegendList if:

  • You're already on React Native's New Architecture (Fabric/Nitro)
  • Blank flashes are unacceptable (media feeds, photo galleries)
  • You want the smoothest possible scrolling for very large datasets
  • Dynamic height items cause issues with FlashList

Methodology

Data sourced from official FlashList documentation (shopify.github.io/flash-list), LegendList documentation (legendapp.com), community benchmarks published on Twitter/X by React Native engineers, GitHub star counts as of February 2026, and performance reports from the React Native Discord. FlatList behavior documented from the official React Native docs and community reports on GitHub issues.


Related: React Native Reanimated vs Moti vs Skia for animation in your list items, or Expo Router vs React Navigation vs Solito for navigation between list screens.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.