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
option
tradeoff
evidence
ScrollView/.map
fine <20 static items only
FlatList
RN baseline virtualization
FlashList
recycling; large RN lists (v2 = New Architecture ONLY; old arch → pin v1.x)
1.3M/wk · ships in 8/34
react-window / react-virtual / HighTable
web virtualization; HighTable handles billion-row tables (viewport-only DOM + scrollbar downscaling)
5.8M/wk · ships in 5/34
Legend List
one virtualization engine for RN + web (v3.0 stable); SectionList + chat/anchoring APIs; signal-driven container pool → very small re-renders
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
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
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.
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.
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.
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).
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).
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).
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.
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.