Skip to content
OTFotf
All posts

FlashList v2 earns its place in every production React Native feed

D
DaveAuthor
7 min read
FlashList v2 earns its place in every production React Native feed

Every production React Native app eventually grows a feed that stutters. The product list, the chat history, the activity timeline — it scrolled fine with two hundred test rows on a simulator, then real users loaded ten thousand items on a three-year-old Android phone and the frame rate fell apart. Blank cells flash during fast scrolls, memory climbs until the OS kills the app, and the one-star reviews mention lag by name.

FlatList ships with React Native and handles simple cases well, but its virtualization model shows its age under production load. Shopify's FlashList was built as a drop-in answer to exactly this problem, and version 2 rebuilds it from the ground up for the new architecture as a JS-only solution. If your roadmap includes the new architecture — and at this point it should — migrating your heaviest lists is one of the highest-use performance moves available. This post is the migration checklist that gets you there without regressions.

Why FlatList stutters in production feeds

FlatList renders only the window of items around the viewport, which sounds sufficient until you look at how it manages that window. It destroys and recreates views as you scroll, it needs size estimates to lay out offscreen content, and wrong estimates produce the classic symptom set: blank areas during momentum scrolling, jumpy scroll position when new items arrive, and layout thrash in lists with mixed item types.

Most teams paper over this with tuning props. They raise windowSize, add getItemLayout, strip components down, memoize render items. That tuning helps, and for modest lists it is enough. But it is manual, fragile, and every new item type reopens the negotiation. The deeper problem is architectural: FlatList's recycling story is weaker than what native list components on either platform can do, so you pay a per-frame tax that no amount of memoization fully erases.

This is also where error tracking earns its keep. Scroll jank rarely throws exceptions, so crash reporters stay quiet while users suffer. Pairing a list migration with release-health monitoring — the approach in our Sentry release checklist for React Native — lets you confirm the new list actually reduced ANRs and low-memory terminations instead of assuming it did.

What FlashList v2 changes

FlashList keeps the FlatList API shape deliberately: in most files you change the component name and the import, and the list works. Underneath, the differences matter. It recycles views instead of destroying them, which removes the blank-cell problem during fast scrolling and cuts memory overhead on long lists. Initial render is optimized for a fast first paint. Full TypeScript support means the migration surfaces type errors at build time rather than blank screens at runtime.

Version 2 adds three things worth knowing about. First, it no longer needs size estimates at all — v1 asked for them, v2 handles item sizing automatically, including dynamic sizes. Second, it maintains visible content position by default, so prepending chat messages or inserting feed items no longer yanks the viewport. Third, it supports multiple recycling pools through getItemType, so lists mixing cards, ads, dividers, and headers keep per-type recycling instead of collapsing to the lowest common denominator. Masonry layouts come along for feeds that need staggered grids.

The one hard constraint: FlashList v2 is new-architecture-only and will not run on the old architecture. If your app still runs the legacy bridge, you either migrate architectures first or stay on FlashList v1, whose documentation Shopify still publishes. Treat the architecture question as step zero of this checklist, not a footnote.

One codebase. iOS, Android, and web.

The Fitness Kit ships with auth, a database, and a backend already connected — no setup. Live demo at fitness-preview.otf-kit.dev.

See the live demo

The migration checklist that avoids regressions

Migrate one list at a time, starting with the heaviest screen in your app — the one with the most item types or the longest data set. A working migration follows this order.

First, confirm the new architecture is enabled in both your debug and release builds, and that your release pipeline distinguishes the two correctly. Environment mistakes here are silent and expensive; the EAS env profiles guide covers the split that keeps secrets and build variants straight.

Second, install FlashList and swap the import on a single screen:

// Before
import { FlatList } from 'react-native';

// After
import { FlashList } from '@shopify/flash-list';
<FlashList
  data={messages}
  renderItem={({ item }) => <MessageRow message={item} />}
  keyExtractor={(item) => item.id}
  getItemType={(item) => item.kind}
/>

Note getItemType: it costs one line and buys per-type recycling pools. Add it whenever a list renders more than one component shape.

Third, delete your size estimates. Remove getItemLayout overrides that existed only to feed FlatList's layout math, and confirm dynamic-height items measure correctly — v2 handles this automatically, but items whose height depends on async content like remote images still need a fixed placeholder height to avoid post-load jumps. Reserve image dimensions from your API or aspect-ratio metadata, not from on-load callbacks.

Fourth, re-verify behaviors that depend on scroll position: pagination triggers (onEndReached thresholds behave slightly differently under recycling), scroll-to-index on deep links, pull-to-refresh offsets, and any analytics that log visible impressions. These are the regressions that slip through when the list itself renders perfectly. If your app works offline, re-run the queue scenarios from our offline-first mutation queue guide against the migrated screen, since optimistic rows inserting at the top exercise exactly the content-position logic v2 changed.

Fifth, test on a low-end Android device with a production build. Debug builds lie about list performance — Hermes bytecode, disabled dev overhead, and release-only view flattening all change the numbers. If you only test on a flagship iPhone in development mode, you have tested nothing.

getItemType and masonry without regret

Two v2 features deserve deliberate design rather than accidental adoption. Multiple recycling pools via getItemType are strictly good: return a stable string per item shape and each pool recycles independently. The failure mode is returning unstable or overly granular types — one type per item id, for example — which defeats pooling entirely. Keep the type set small and semantic: message, date-divider, ad-card, not per-user variants.

Masonry support tempts feed redesigns, and staggered grids genuinely suit discovery surfaces. But masonry plus dynamic image heights plus pagination is the hardest combination in list rendering on any platform. Ship masonry only with reserved image dimensions and a capped column count, and measure scroll performance before calling it done. A two-column grid with known aspect ratios is a safe default; a four-column staggered wall of user uploads is a performance project, not a prop.

Measure before and after

A migration without numbers is a vibe change. Capture baselines on the old list before touching code: time to first paint of the screen, dropped-frame percentage during a scripted fast scroll, peak memory after scrolling the full data set, and the ANR or hang rate from your error tracker. Then capture the same four on the release build with FlashList.

You do not need a lab. A repeatable script — launch, open the heaviest feed, fling three times, background the app — run five times per variant on the same device gives you a defensible comparison. If the numbers do not move, check that you are actually running the new architecture and a release build before blaming the library. The most common failed migration I have seen was a debug-build comparison where both variants looked equally bad.

When to stay on FlatList

FlashList is not mandatory. Short lists under a few dozen items with a single item type will not show a meaningful difference, and the dependency adds install weight and native-module surface you then own. Settings screens, short menus, and small pickers should stay on FlatList. If your app cannot move to the new architecture yet, FlashList v1 remains available, but weigh that against the cost of migrating twice — architecture first, then the list, may be the honest sequence.

The rule of thumb: migrate lists where users scroll fast through heterogeneous content — feeds, chats, catalogs, timelines. Leave everything else alone until a measurement says otherwise.

Sources

react-nativecross-platform
OTF Fitness Kit

Stop wiring. Start shipping.

  • Login, database, and backend already connected — nothing to set up
  • iOS + Android + web from one codebase
  • AI configs pre-tuned + 40+ tested prompts included