react-brain
Browse decisions
React & language foundations 3App architecture 10UI 13Platform & native 9Build, test, observe, secure 6AI in React apps 3stack composerdecision recordscensusstaleness bencharchitecturechangelogmethodologyroadmap

entriesui · verified 2026-08-18 · react + react-native

Lists & virtualization

reviewedconfidence: hightrack: heldthis tier holds 83% on the public scorecard (6/11 graded · 0 overturned) →

re-verified 5× — 2026-08-18 · 2026-08-04 · 2026-07-13 · 2026-07-10 · 2026-07-06 · changelog

recommendation

RN: FlashList for large lists, FlatList otherwise, ScrollView only for <20 static items. Web: TanStack Virtual (react-virtual) or react-window — both current (3.14.9 / 2.3.0, verified 2026-08-18), pick on API taste.

  • huge web tables (100k+ rows) → HighTable
  • one list engine across RN + web → Legend List
  • old-architecture RN app → FlashList v2 is New-Arch-only; pin FlashList v1.x or stay on FlatList
  • text-heavy RN feeds where hundreds of flat labels mount at once → try react-native-plain-text for those labels (renders straight to UILabel/TextView, skipping RN's text layout pipeline; one string, one style, Fabric-only, 0.7.x beta) and keep <Text> wherever you need nesting, mixed styles, press handling or selection — the two mix in one screen
  • exact thresholds / perf → react-native-best-practices owns the rules

Options & tradeoffs

the field considered — and why each one isn’t the default here

optiontradeoffevidence
ScrollView/.mapfine <20 static items only
FlatListRN baseline virtualization
FlashListrecycling; large RN lists (v2 = New Architecture ONLY; old arch → pin v1.x)1.3M/wk · ships in 8/34
react-window / react-virtual / HighTableweb virtualization; HighTable handles billion-row tables (viewport-only DOM + scrollbar downscaling)5.8M/wk · ships in 5/34
Legend Listone virtualization engine for RN + web (v3.0 stable); SectionList + chat/anchoring APIs; signal-driven container pool → very small re-renders283k/wk · ships in 2/34

evidence: npm weekly downloads (signals snapshot) · “ships in n/D” = adoption across the production-app census, honest denominators

Migration lines

version and deprecation lines this decision tracks — for YOUR repo's sequenced plan: the doctor or npx -y @heart-it/react-brain migrate .

@shopify/flash-list < 2.0.0 → FlashList v2 [upgrade · effort M]

v2 is a ground-up New-Architecture rewrite (recycling, progressive rendering, pre-paint correction) and is New-Arch-ONLY — on the old architecture stay pinned to v1.x until the RN ladder is done

shopify.engineering/flashlist-v2

@tanstack/react-table < 9.0.0 → TanStack Table v9 [upgrade · effort M]

v9 went stable 2026-08-04 and moves reactivity underneath the Table APIs — per-feature atoms behind a TableReactivityBindings contract, so grid re-renders track the state a cell actually uses instead of whole-table state; callers keep passing ordinary values and calling the same Table methods, so this is a version-line move rather than a rewrite

tanstack.com/blog/tanstack-table-v9-reactivity · registry.npmjs.org/@tanstack/react-table/latest

npm weekly downloads (from the corpus's last signals run): @shopify/flash-list 1.3M · @legendapp/list 283k · react-window 5.8M · @tanstack/react-virtual 18.4M · @tanstack/react-table 15.9M

Verified notes

PLAIN TEXT PROMOTED skip→when-clause 2026-08-18 (2nd independent signal — TWiR #293 then RN Rewind #52 — which was the reopen condition recorded at the first skip; facts verified vs the repo README + npm 0.7.1): react-native-plain-text is a narrow micro-optimization, not a list engine — it bypasses RN's text layout pipeline by rendering to the platform's native label widget, and buys that with a hard constraint (single string, single style; no nested <Text>, no press handling or selection). It requires the New Architecture and does not run in Expo Go. Treat it as cell-content tuning AFTER virtualization is right, and measure before adopting. V9 ALSO MOVED THE SCALE CEILING (added 2026-08-18 on spot-check — this post had been carried as a `cap` skip since React Status #486, and re-reading it overturned that: it is not a duplicate of the reactivity story, it changes a threshold this entry recommends on). V8 built every row/cell/header object with its own copy of every method, each carrying its own closure scope, so memory scaled with objects created; V9 hangs those methods on shared prototypes cached per table. Measured on the authors' Playwright/CDP benchmark: up to ~90% less memory on large tables, moving the practical browser ceiling from ~1–1.5M rows before hitting ~4GB to roughly 10–16M. Two honest edges they state themselves — small tables (10 rows × 8 cols, all features) use slightly MORE memory in v9 because of added memoization, and one breaking change came with the refactor. The consequence here: "huge web tables → HighTable" is now a call about DOM/UX at that size, not about Table running out of memory first. TANSTACK TABLE v9 IS STABLE (9.0.0 on npm 2026-08-04, verified vs the registry — the beta line had been carried as a skip since #291). v9 moves REACTIVITY underneath the Table APIs: instead of one table-state object that adapters wrap from the outside, feature state is split into separate atoms (pagination, row selection, column sizing, …) behind a shared `TableReactivityBindings` contract, so each of its 9 framework adapters plugs in native signals (Angular/Solid/Vue) or TanStack Store. The point is grain — selecting a row updates that checkbox and the selected-row counter without making every cell depend on whole-table state. Callers still pass ordinary values and call the same Table methods, so this is an upgrade, not a rewrite.

Canonical reading

Editorial annotations on why each piece matters — the articles themselves are the originals; read them there.

How to Build the Fastest Apps: Break the Rules (App.js Conf 2026)Jay Meistrich — author of Legend List

Why Legend List is fast, from its author: it mounts a fixed pool of absolutely-positioned containers once and never re-renders that array again — instead it *signals* individual containers to re-render themselves at a new position/item as you scroll, and turns frequent size changes into animated-style updates rather than list re-renders. The generalizable lesson ('render once'): keep each render tiny by pushing it to the leaf that actually changed. Complements the peterp.me engine walkthrough with the same-source mechanism; announces v3.0 stable (RN + web). Vendor talk.

FlashList v2: a ground-up rewrite for React Native's New ArchitectureTalha Naqvi (Shopify Engineering)

The maintainer's architecture deep-dive on FlashList's New-Architecture rewrite — view recycling, progressive rendering, layout prediction, and pre-paint correction via synchronous measurement — explaining the recycling machinery from first principles, not just the API.

React windowing vs. component recyclingIskander Samatov (LogRocket)

Cleanly separates the two long-list strategies — windowing (render only the viewport, mount/unmount on scroll) vs recycling (reuse nodes by reassigning keys) — and when to escalate. Bridges web (react-window) and RN (FlashList).

What's actually happening inside Legend ListPeter Piekarczyk

Explains Legend List's virtualization formula — estimated item size, draw distance, container-pool ratio, asymmetric 1.5x/0.5x buffering. A concrete mental model for list recycling performance (complements the FlashList piece).

Virtual scrolling for billions of rows (techniques from HighTable)Sylvain Lesage

The five techniques behind HighTable — lazy slicing, the ~17M-px canvas-height ceiling with downscaled scrollbars, dual local/global scroll modes, decoupled axes. The web huge-table counterpart to the RN list pieces above (HighTable is a listed option here).

Reactivity in TanStack Table V9TanStack

The architecture rationale behind the v9 line this entry's note records: v8 kept one stable table instance and let each adapter connect it to a framework's update model from the outside, which makes any state change look like whole-table churn. V9 puts a reactive graph underneath instead — per-feature atoms plus a `TableReactivityBindings` contract each adapter fills with its framework's own primitives — so a row selection updates that row and the counter, not every cell. The generalizable lesson matches this entry's Legend List readings: the cost of a big grid is what you make re-render, so put the subscription grain at the leaf.

Sources

Depth (in-domain rules) is owned by the react-native-best-practices skill — this entry is selection breadth.

The full explanation

The reviewed long-form essay behind this entry — the why, not a how-to. Also on GitHub.

About lists & virtualization in React & React Native

Diataxis: Explanation. This page builds understanding of why long lists need virtualization and which strategy each tool uses. It is not a tutorial, and the exact thresholds (when FlashList, item-size rules) are owned by react-native-best-practices. Read this for the why.

The one fact that organises everything: cost scales with mounted nodes, not data length

A list is cheap or expensive based on how many view/DOM nodes it mounts, not how many items the array holds. Render 10,000 rows naively and you create 10,000 nodes — layout, memory, and scroll all collapse. The whole field exists to break that coupling: render only what's on (or near) the screen. Two strategies do it, and every library here is one of them:

  • Windowing — mount only the items in the viewport (plus a small buffer); as you scroll, unmount what leaves and mount what enters. The node count stays ~constant regardless of data size. (FlatList, react-window, react-virtual.)
  • Recycling — keep a small fixed pool of views and re-bind them to new data as they scroll in/out, instead of unmounting and remounting. Cheaper still for big, uniform lists because it avoids mount/unmount churn. (FlashList.)

So the first question is never "which list lib" — it's "how many nodes will this mount, and is windowing enough or do I need recycling?"

The default, and why

React Native: FlashList for large lists, FlatList otherwise, ScrollView only for <~20 static items. Web: react-virtual / react-window.

The RN ladder is about matching strategy to scale. ScrollView mounts everything — fine for a handful of static rows, a foot-gun beyond that. FlatList adds windowing — the correct baseline for most lists. FlashList adds recycling — the win for large lists, where mount/unmount churn dominates (its v2 is a New-Architecture rewrite; see the reading). On the web, the data isn't native views but DOM nodes, and the same logic applies: react-window / react-virtual window the viewport.

The landscape, and when each one wins

ScrollView / .map() — no virtualization; every item mounts. Correct only for small, static lists (<~20). Using it for real data is the most common list mistake.

FlatList — RN's built-in windowing. The right default; tune-able, mature, and enough for the majority of lists.

FlashList — recycling for large RN lists. Wins when lists are long and rows are uniform enough to reuse; v2's ground-up New-Architecture rewrite handles recycling, progressive rendering, and layout prediction (reading). Reach for it at scale, not reflexively. Note that v2 runs only on the New Architecture — old-arch apps must pin v1.x (or stay on FlatList).

react-window / react-virtual / HighTable (web) — web windowing. react-window/react-virtual cover normal long lists; HighTable handles billion-row tables with tricks the others don't (working around the ~17M-px canvas-height ceiling with downscaled scrollbars, decoupled axes — reading). Reach for HighTable specifically for huge tabular data.

Legend List — one virtualization engine across RN + web (v3 adds SectionList + chat APIs). The pick when you want a single list mental model on both platforms.

Tradeoffs and failure modes to name out loud

  • ScrollView for real data. The cardinal sin: it mounts everything, so it's smooth at 15 rows and janks at 200. Match the tool to the count.
  • FlashList reflexively. Recycling shines for large, uniform lists; for short or wildly heterogeneous lists, FlatList's windowing is simpler and fine. Don't reach for recycling before you have the scale that needs it.
  • Fighting recycling with unstable items. Recycled rows assume reuse; per-row unstable state, unkeyed content, or wildly variable heights undercut the pool and reintroduce churn.
  • Ignoring the thresholds. "How many items before FlashList," item-size estimates, and render-perf rules are exact and owned by react-native-best-practices — this page gives the model, that skill gives the numbers.

How it interacts with the rest of the stack

  • Charts/graphics (RB-E-CHARTS). Huge-data rendering shares DNA — at extreme scale (charts, billion-row tables) the answer shifts from DOM/view virtualization toward canvas/GPU rendering.
  • Data (RB-E-DATA). Long lists usually mean paginated/infinite server data; the list's windowing pairs with the data layer's useInfiniteQuery-style fetching.
  • Perf depth (react-native-best-practices). Exact thresholds, item-layout, and the RN-RULE-USE-FLASHLIST guidance live there.

In one paragraph

A list's cost is the nodes it mounts, not the data length — so render only what's on screen, via windowing (mount the viewport, unmount the rest) or recycling (reuse a fixed view pool). On React Native: ScrollView only for tiny static lists, FlatList (windowing) as the baseline, FlashList (recycling) at scale. On the web: react-window/react-virtual for normal lists, HighTable for billion-row tables; Legend List when you want one engine across both. Don't use ScrollView for real data, don't reach for recycling before you need it, and get exact thresholds from react-native-best-practices.


See also: RB-E-CHARTS (canvas/GPU rendering at extreme data scale), RB-E-DATA (paginated / infinite data behind long lists). Exact thresholds + render-perf rules: the react-native-best-practices skill.

Related in ui: styling · component-libs · animation · a11y · i18n · charts · editors · svg · maps · calendars · sheets · polish