FlashList vs FlatList
Shopify FlashList trade-offs, recycling, and migration checklist - when the default list is not fast enough.
Search across all documentation pages
Shopify FlashList trade-offs, recycling, and migration checklist - when the default list is not fast enough.
Quick-reference recipe card - copy-paste ready.
import { FlashList } from "@shopify/flash-list";
import { StyleSheet, Text, View } from "react-native";
interface Post {
id: string;
title: string;
}
const ESTIMATED_ROW_HEIGHT = 88;
export function PostFeed({ posts }: { posts: Post[] }) {
return (
<FlashList
data={posts}
estimatedItemSize={ESTIMATED_ROW_HEIGHT}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.row}>
<Text style={styles.title} numberOfLines={2}>
{item.title}
</Text>
</View>
)}
ItemSeparatorComponent={() => <View style={styles.separator} />}
/>
);
}
const styles = StyleSheet.create({
row: { paddingHorizontal: 16, paddingVertical: 12, backgroundColor: "#fff" },
title: { fontSize: 16, fontWeight: "600", color: "#0f172a" },
separator: { height: 8 },
});When to reach for this: Image-heavy feeds, chat timelines, or any list where profiling shows frame drops during fast scroll on mid-range Android devices.
import { memo, useCallback } from "react";
import { FlashList } from "@shopify/flash-list";
import { Image, StyleSheet, Text, View } from "react-native";
interface Product {
id: string;
name: string;
price: string;
imageUrl: string;
}
const PRODUCTS: Product[] = Array.from({ length: 200 }, (_, i) => ({
id: `p-${i}`,
name: `Product ${i + 1}`,
price: `$${(9.99 + (i % 50)).toFixed(2)}`,
imageUrl: `https://picsum.photos/seed/${i}/120/120`,
}));
const ESTIMATED_ROW_HEIGHT = 96;
const ProductRow = memo(function ProductRow({ item }: { item: Product }) {
return (
<View style={styles.row}>
<Image source={{ uri: item.imageUrl }} style={styles.thumb} />
<View style={styles.meta}>
<Text style={styles.name} numberOfLines={1}>
{item.name}
</Text>
<Text style={styles.price}>{item.price}</Text>
</View>
</View>
);
});
export default function ProductFeedScreen() {
const renderItem = useCallback(
({ item }: { item: Product }) => <ProductRow item={item} />,
[],
);
return (
<View style={styles.screen}>
<FlashList
data={PRODUCTS}
estimatedItemSize={ESTIMATED_ROW_HEIGHT}
keyExtractor={(item) => item.id}
renderItem={renderItem}
drawDistance={250}
contentContainerStyle={styles.list}
/>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, backgroundColor: "#f8fafc" },
list: { paddingVertical: 8 },
row: {
flexDirection: "row",
alignItems: "center",
gap: 12,
paddingHorizontal: 16,
paddingVertical: 10,
backgroundColor: "#fff",
minHeight: ESTIMATED_ROW_HEIGHT,
},
thumb: { width: 72, height: 72, borderRadius: 8, backgroundColor: "#e2e8f0" },
meta: { flex: 1 },
name: { fontSize: 16, fontWeight: "600", color: "#0f172a" },
price: { fontSize: 14, color: "#2563eb", marginTop: 4 },
});What this demonstrates:
estimatedItemSize set to the typical row height - required for FlashList layout math.minHeight on the row aligns rendered size with the estimate for stable scroll.memo on ProductRow pairs with view recycling - rows must tolerate reuse without stale state.drawDistance={250} pre-renders slightly ahead of the viewport to reduce blank flashes during fast flick scroll.keyExtractor / renderItem patterns as FlatList - migration is mostly prop additions.estimatedItemSize because it lays out content before every row is measured - the estimate drives offset calculations until real measurement arrives.data, renderItem, keyExtractor, separators, headers, onEndReached.| Aspect | FlatList | FlashList |
|---|---|---|
| Recycling | Mount/unmount rows | Rebinds a fixed pool of views |
| Size hint | Optional getItemLayout | Required estimatedItemSize |
| Variable height | Works; remeasures as needed | Works; tune estimate + overrideItemLayout |
| Bundle | Built into RN | @shopify/flash-list dependency |
| API surface | Superset (mature) | Near drop-in; some prop differences |
| Best fit | Short/medium lists, settings | Long feeds, images, chat, marketplace grids |
npx expo install @shopify/flash-listFlatList → FlashList from @shopify/flash-listestimatedItemSize: Measure a typical row in DevTools or design specs (include padding).item props only.minHeight or fixed height close to the estimate when possible.overrideItemLayout per item type or separate lists per layout variant.onEndReached: Threshold behavior is similar but verify pagination guards.FlashList with sticky headers or keep SectionList until you need the perf win.console.time around scroll sessions.// Before
import { FlatList } from "react-native";
<FlatList data={items} renderItem={...} keyExtractor={...} />
// After
import { FlashList } from "@shopify/flash-list";
<FlashList
data={items}
estimatedItemSize={72}
renderItem={...}
keyExtractor={...}
/>| Prop | Purpose |
|---|---|
estimatedItemSize | Average row height in dp - required |
drawDistance | Pixels beyond viewport to pre-render (default ~250) |
overrideItemLayout | Per-index size override for heterogeneous rows |
estimatedListSize | { height, width } when list size is known early |
getItemType | Return a type string so recycling pools separate layouts |
<FlashList
data={items}
estimatedItemSize={120}
getItemType={(item) => (item.pinned ? "pinned" : "normal")}
overrideItemLayout={(layout, item) => {
layout.size = item.pinned ? 160 : 96;
}}
renderItem={renderItem}
/>useState inside a row can show the previous item's state for a frame.useEffect without proper deps fire on the wrong item.item data or parent maps (expandedIds); rows should be pure functions of props.import type { FlashListProps, ListRenderItem } from "@shopify/flash-list";
interface Article {
id: string;
headline: string;
}
type ArticleListProps = Pick<
FlashListProps<Article>,
"data" | "onEndReached" | "refreshing" | "onRefresh"
> & {
onPress: (article: Article) => void;
};
function ArticleList({ data, onPress, ...rest }: ArticleListProps) {
const renderItem: ListRenderItem<Article> = useCallback(
({ item }) => <ArticleRow article={item} onPress={() => onPress(item)} />,
[onPress],
);
return (
<FlashList
data={data}
estimatedItemSize={104}
keyExtractor={(item) => item.id}
renderItem={renderItem}
{...rest}
/>
);
}@shopify/flash-list, not react-native.FlashListProps<T> mirrors FlatListProps<T> for most list props.| Parameter | Type | Description |
|---|---|---|
estimatedItemSize | number | Average item height in density-independent pixels - required |
data | readonly T[] | Source array (same as FlatList) |
renderItem | ListRenderItem<T> | Row renderer - must be stable for recycling |
drawDistance | number | Pre-render distance beyond viewport edges |
overrideItemLayout | (layout, item, index) => void | Set layout.size for known per-item heights |
getItemType | (item, index) => string | Separates recycle pools per layout variant |
Missing estimatedItemSize - FlashList warns and falls back to poor layout guesses; scroll feels broken. Fix: Always set it; measure real rows in the simulator.
Estimate far from reality - A 200px estimate on 80px rows causes jump when measurement corrects. Fix: Measure p50 row height; use overrideItemLayout for outliers.
Stateful row components - useState for "liked" inside the row shows the previous item's like state after recycle. Fix: Store interaction state in parent/item or extraData maps.
Migrating without memo - Recycling magnifies unnecessary re-renders inside rows. Fix: memo row components; stable renderItem with useCallback.
Using FlashList for 10 settings rows - Extra dependency and tuning for no measurable gain. Fix: Keep FlatList or ScrollView until profiling proves need.
Ignoring image dimensions - Images loading async change row height after first layout, causing jump on both list types; worse when estimate is wrong. Fix: Fixed width/height on Image, placeholder background, or known aspect ratio.
Assuming 100% API parity - Some edge props differ; test ListHeaderComponent, numColumns, and nested scroll. Fix: Read @shopify/flash-list docs for your version; run migration checklist on device.
| Alternative | Use When | Don't Use When |
|---|---|---|
FlatList | Default lists, settings, < 100 rows, no perf complaints | Profiling shows scroll jank on target devices |
FlashList | Long feeds, images, chat, marketplace | Trivial lists where bundle size matters more |
SectionList | Grouped data with sticky section headers | Single-type flat feed - FlashList is simpler |
Legend List / other community lists | Evaluating alternatives to FlashList | You want Shopify-maintained Expo compatibility first |
ScrollView + map | Very short static content | Any list that scrolls for seconds at 60 fps |
Nearly - swap the import, add estimatedItemSize, and audit row components for recycling safety. Most props (data, renderItem, keyExtractor, onEndReached, refresh) work the same. Test your specific header/footer and multi-column layouts.
FlashList positions rows before all are measured. The estimate drives scroll offset math until each row's true height is known. Without it, the list cannot virtualize efficiently.
Measure a typical rendered row in the layout inspector, including padding and separators. Use the median height if rows vary slightly. For mixed types, use getItemType + overrideItemLayout.
Instead of destroying a row view when it scrolls off screen, FlashList rebinds it to a new item's data. The same native view tree is reused - like UITableView cell reuse on iOS. Row components must not hold stale local state.
Yes - npx expo install @shopify/flash-list pins a compatible version for your SDK. No custom native code required in standard Expo workflows.
FlashList supports sticky headers and heterogeneous layouts, but SectionList is still the ergonomic choice for { title, data } grouping. Migrate item rendering to FlashList patterns when the flat feed is the bottleneck.
Distance in pixels beyond the visible viewport where FlashList pre-renders items. Higher values reduce blank flashes during fast scroll at the cost of more work per frame. Default is usually fine; raise on image-heavy feeds if you see flicker.
FlashList uses estimatedItemSize and overrideItemLayout instead. overrideItemLayout lets you set per-index layout.size when heights are known without measuring.
Yes - grid layouts are supported. Set numColumns and ensure estimatedItemSize reflects row height (not cell width). Test horizontal spacing and recycle pools for grid cells.
estimatedItemSize closer to real height.minHeight on rows.overrideItemLayout for known tall/short variants.Yes - especially with recycling. Combine memo on the row, stable renderItem, and props-only state. See List Performance Tuning.
FlashList supports the same pattern - pass external state that affects row rendering so recycled views update when selection or theme changes.
For very short lists, FlatList avoids estimate tuning and the extra package. Measure - do not migrate preemptively.
See @shopify/flash-list in Essential Libraries for install, config plugins, and advanced props.
Same onEndReached / onEndReachedThreshold pattern as FlatList. Guard fetches with a loading ref. See Infinite Scroll & Pagination.
memo, stable handlers, and windowSize concepts that apply to both listsStack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026