Mobile — App Performance Optimization

FlashList vs FlatList: List Performance at Scale

Direct answer

FlatList mounts and unmounts row components as you scroll, which causes blank cells and dropped frames on long or complex lists. FlashList (by Shopify) recycles rendered cells instead — the component instances stay mounted and get new data — which is dramatically cheaper. For feeds beyond a few hundred items or rows with images and nested layouts, FlashList is the right default; migration from FlatList is close to drop-in.

Every React Native feed eventually hits the same wall: the list that felt fine with 50 demo items drops frames and flashes blank cells with 5,000 real ones. This guide explains why FlatList struggles, what FlashList actually does differently, and the two migration gotchas that catch teams — with the measurement approach to prove the win.

Key facts, with sources

  • The median crash-free session rate across mobile apps in 2025 was 99.95%, with top-performing teams at the 75th percentile holding 99.99%. (Luciq Mobile App Stability Outlook 2025)
  • Compared with JavaScriptCore, the Hermes engine cuts typical React Native app startup from about 4.5 seconds to about 2.0 seconds, memory from about 185 MB to 136 MB, and engine app-size overhead from about 12 MB to 8 MB. (OneUptime Blog)
  • Real-world production migrations to React Native's New Architecture report around 43% faster cold starts, 39% faster rendering, and 26% lower memory usage. (RapidNative)
  • Shopify's performance bar for its React Native apps is critical screens loading in under 500 milliseconds at the 75th percentile, alongside over 99.9% crash-free sessions. (Shopify Engineering)
  • Published optimization case studies include Discord cutting app startup time in half and Coinbase reporting an 80% funnel performance improvement after its React Native rewrite. (CatDoes)

Why FlatList struggles at scale

FlatList is a virtualized list: it renders only a window of items around the viewport and unmounts everything else. The problem is what happens during fast scrolling — new rows must be mounted from scratch, which means creating component instances, running layout, and committing native views, all inside the frame budget. When a row is expensive (images, gradients, nested Text), mounting can't keep up with scroll velocity, and you get FlatList's signature failure: blank regions that fill in late. Tuning windowSize, maxToRenderPerBatch, and getItemLayout helps at the margins, but the fundamental cost — full mount and unmount per row per scroll pass — never goes away.

What FlashList does differently: recycling

FlashList keeps a pool of rendered cell components and recycles them: when a row scrolls out of view, its component instance isn't destroyed — it's handed the next item's data and repositioned. React reconciles the new props against an already-mounted tree, which is far cheaper than mounting fresh. This is the same technique native UITableView and RecyclerView have used for a decade, brought to React Native.

FlashList v2 was rewritten for React Native's new architecture and dropped v1's biggest ergonomic tax: you no longer need to supply estimatedItemSize — the list measures and manages layout itself. If you evaluated FlashList v1 and walked away because of estimate tuning, that objection is gone.

Migration: mostly drop-in, two real gotchas

The API is deliberately FlatList-shaped, so most migrations are an import change. The two things that actually bite: first, recycled cells keep component state — if a row holds local useState (an expanded flag, a checkbox), that state travels to whichever item the cell is recycled for. Derive state from item data, or reset it in an effect keyed on the item id. Second, don't put a key prop on the row root or generate keys inside renderItem — keys defeat recycling and silently give you FlatList performance again; use keyExtractor and let FlashList manage identity.

Migrating a feed to FlashList
import { FlashList } from "@shopify/flash-list";

export function Feed({ posts }: { posts: Post[] }) {
  return (
    <FlashList
      data={posts}
      renderItem={({ item }) => <PostCard post={item} />}
      keyExtractor={(item) => item.id}
      // v2: no estimatedItemSize needed
    />
  );
}

// Gotcha: recycled cells keep local state.
// BAD  — `expanded` follows the recycled cell to a different post:
//   const [expanded, setExpanded] = useState(false);
// GOOD — reset when the item changes:
function PostCard({ post }: { post: Post }) {
  const [expanded, setExpanded] = useState(false);
  useEffect(() => setExpanded(false), [post.id]);
  // ...
}

When FlatList is still fine

Not every list needs replacing. A settings screen with 20 rows, a picker with 50 options, or any list where every item fits in two viewports will never show the difference — FlatList is built in, adds no dependency, and behaves predictably. The switch pays off on long, scrollable content feeds: social timelines, product catalogs, chat histories, search results. A reasonable team rule: FlatList for bounded UI lists, FlashList for anything fed by a paginated API.

Measure before and after, in release mode

Two numbers tell the story: JS-thread FPS during a fast scroll (the dev Perf Monitor shows it live) and how often users see blank space. Profile in release builds on a mid-range Android device — dev-mode numbers are noise, and the phones in your team's pockets are faster than your users'. For production visibility, performance monitoring like Sentry or Firebase Performance will show slow-frame rates across your real device fleet, which is how you confirm the win actually shipped to users rather than just to your simulator.

When to hire senior help

Bring in a senior performance specialist when your crash-free rate sits below roughly 99.9%, startup exceeds a couple of seconds on mid-range Android, or the team lacks profiling experience with Hermes, the JS thread, and native tooling. Performance rescue work is diagnostic in nature, so a short expert engagement typically finds the handful of hot spots that in-house teams spend months guessing at. If your stack includes React Native + Python + AI, a senior engineer who owns the full product beats coordinating multiple juniors.

Bottom line

Dhairya Senjaliya ships Mobile — App Performance Optimization projects worldwide — book a scoping call to discuss your specific situation.

Common pitfalls to avoid

  • Profiling only on high-end iPhones in dev mode, then discovering the real user base on low-end Android devices experiences multi-second startups and dropped frames
  • Rendering long feeds with ScrollView or an untuned FlatList instead of a virtualized list like FlashList, blocking the JS thread during scroll
  • Passing new inline functions and object literals on every render without memoization, causing cascading re-renders that never show up until lists grow
  • Ignoring JS bundle size and startup path, shipping no lazy loading or inline requires, so time-to-interactive balloons as the app grows

Frequently asked questions

Is FlashList a drop-in replacement for FlatList?

Very close. It intentionally mirrors FlatList's props (data, renderItem, keyExtractor, onEndReached), so most migrations are an import swap. The main behavioral difference is cell recycling: rows keep their component instances, so local row state must be derived from item data or reset when the item changes.

Do I still need estimatedItemSize with FlashList?

Not in FlashList v2 — it was rebuilt for React Native's new architecture and handles measurement itself. v1 required estimatedItemSize, and poor estimates were the top cause of disappointing first impressions.

Does FlashList work with Expo?

Yes — @shopify/flash-list is supported in Expo projects (npx expo install @shopify/flash-list) and works with EAS builds. No config plugin or native code changes are needed.

Which performance metrics should we actually track?

Crash-free session rate (the 2025 median is 99.95%, and below 99.8% is a red flag), cold start time, time-to-interactive, P75 screen-load time, and frame rate during scrolling. These need real-user monitoring in production, not just lab measurements.

Why is our React Native app slow on Android but fine on iOS?

The usual causes are testing only on flagship devices, JS-thread-blocking work, unvirtualized lists, and running an old React Native architecture or the JSC engine. Hermes and New Architecture migrations show measured gains of roughly 26 to 43% on memory and cold start, so upgrading the foundation is often the highest-leverage fix.

Does performance really affect revenue or is it an engineering vanity metric?

It compounds directly into acquisition and retention: crashes and slowness drive uninstalls and one-star reviews, and lower ratings measurably cut store conversion rates. That is why top consumer apps hold themselves to 99.99% crash-free sessions and sub-500ms screen loads.

Bottom line: Dhairya Senjaliya ships Mobile — App Performance Optimization projects worldwide. Book a scoping call at https://dhairyasenjaliya.com/#book-call.

Sources

Related guides

Keep up with new guides

New deep-dive guides on React Native, Python, and AI ship regularly. Subscribe via RSS or follow on LinkedIn.

Want help implementing this?

30-minute scoping call · Clear milestones · Senior engineer ownership