TanStack Virtual vs react-window vs react-virtuoso 2026
TL;DR
For most new projects: react-virtuoso is the pragmatic choice — the richest API with built-in support for dynamic heights, groups, sticky headers, and infinite scroll out of the box. @tanstack/react-virtual is the right choice if you want maximum flexibility and zero opinions (headless). react-window is mature and battle-tested but shows its age with fixed-size-only limitations and no active development.
Key Takeaways
- react-window: ~1.9M weekly downloads — stable but no longer actively developed by maintainer
- react-virtuoso: ~2.1M weekly downloads — richest API, best for complex use cases
- @tanstack/react-virtual: ~1.3M weekly downloads — most flexible, headless, pairs well with TanStack ecosystem
- Dynamic row heights (variable size items): react-window requires workarounds; @tanstack/react-virtual and react-virtuoso handle natively
- react-virtuoso wins for grids, groups, sticky headers, and table virtualization
- @tanstack/react-virtual wins for custom implementations and non-React environments
Why Virtualization Matters
Rendering 10,000 DOM nodes destroys performance. Virtualization renders only the visible items:
Without virtualization: 10,000 <li> in DOM → 200ms initial render, jank on scroll
With virtualization: ~20 <li> in DOM at any time → <10ms render, smooth scroll
The tradeoff: complexity, SSR considerations, accessibility challenges.
Download Trends
| Package | Weekly Downloads | Last Major Version | Bundle Size |
|---|---|---|---|
react-window | ~1.9M | 1.8 (2019) | ~6KB |
react-virtuoso | ~2.1M | 4.x (2024) | ~17KB |
@tanstack/react-virtual | ~1.3M | 3.x (2024) | ~5KB |
@tanstack/virtual-core | ~1.3M | 3.x (2024) | ~4KB |
react-window
Created by Brian Vaughn (React core team), react-window was the gold standard for list virtualization from 2019–2022. It's stable, well-documented, and used in production by thousands of apps.
import { FixedSizeList, VariableSizeList } from "react-window"
// Fixed height rows (simplest case):
function FixedList({ items }: { items: string[] }) {
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
<div style={style}>{items[index]}</div>
)
return (
<FixedSizeList
height={400}
itemCount={items.length}
itemSize={50}
width={600}
>
{Row}
</FixedSizeList>
)
}
// Variable height rows (requires pre-measuring heights):
const getItemSize = (index: number) => heights[index] // You must know this upfront
function VariableList({ items }: { items: string[] }) {
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
<div style={style}>{items[index]}</div>
)
return (
<VariableSizeList
height={400}
itemCount={items.length}
itemSize={getItemSize} // Must return size synchronously
width={600}
>
{Row}
</VariableSizeList>
)
}
The react-window problem:
For variable-height items (common in chat apps, feeds, or any dynamic content), you must know all item heights upfront. This is rarely possible with real data. The community workaround (react-virtualized-auto-sizer + ResizeObserver) works but is awkward.
react-window's maintainer has signaled they won't be adding these features. The library is "done."
@tanstack/react-virtual
Part of the TanStack suite (TanStack Query, TanStack Table, TanStack Router). v3 was a complete rewrite with a headless, framework-agnostic core.
The central architectural decision in @tanstack/react-virtual is headlessness. Unlike react-virtuoso — which ships components like <Virtuoso>, <GroupedVirtuoso>, and <TableVirtuoso> — @tanstack/react-virtual gives you a single useVirtualizer hook that returns positioning data. You are responsible for rendering the scroll container, the inner height container, and each virtual item using absolute positioning. This approach means the library has zero opinions about styling, markup structure, or component hierarchy. It also means more code to write for common patterns.
The trade-off is intentional. For teams already using TanStack Query for server state and TanStack Table for structured data display, the consistent headless philosophy means you're not mixing two different mental models. The headless core also ships as @tanstack/virtual-core for teams building virtualization in Vue, Solid, or Svelte — the React-specific package is a thin wrapper around framework-agnostic primitives.
The measureElement API deserves special attention. Pre-v3, dynamic height virtualization required measuring item heights before rendering, which meant either hardcoding heights or using complex IntersectionObserver setups. The measureElement ref-based API lets each item report its actual measured height after render, automatically recalculating subsequent item positions. This eliminates an entire category of virtualization boilerplate:
import { useVirtualizer } from "@tanstack/react-virtual"
import { useRef } from "react"
function VirtualList({ items }: { items: string[] }) {
const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50, // Estimate; actual size measured after render
overscan: 5, // Render 5 extra items outside viewport
})
return (
<div
ref={parentRef}
style={{ height: 400, overflow: "auto" }}
>
{/* Total height maintains scrollbar position */}
<div style={{ height: virtualizer.getTotalSize(), position: "relative" }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
ref={virtualizer.measureElement} // Auto-measures actual height
style={{
position: "absolute",
top: 0,
transform: `translateY(${virtualItem.start}px)`,
}}
>
{items[virtualItem.index]}
</div>
))}
</div>
</div>
)
}
Dynamic heights with measureElement:
The measureElement ref automatically measures each item after render and recalculates positions. This means you can have arbitrary-height content without pre-measuring anything:
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 100, // Just an estimate — real sizes measured on render
})
// Each item auto-reports its actual height
return (
<div ref={virtualizer.measureElement} key={virtualItem.key}>
<ExpandableCard item={items[virtualItem.index]} />
</div>
)
Framework-agnostic core:
import { Virtualizer } from "@tanstack/virtual-core"
// Works with any framework (Vue, Solid, Svelte) via the core package
const virtualizer = new Virtualizer({
count: 10000,
getScrollElement: () => scrollElement,
estimateSize: () => 50,
observeElementRect: observeElementRect,
observeElementOffset: observeElementOffset,
scrollToFn: elementScroll,
onChange: (instance) => render(instance.getVirtualItems()),
})
react-virtuoso
react-virtuoso takes the opposite approach from @tanstack/react-virtual — it's batteries-included:
import { Virtuoso, GroupedVirtuoso, TableVirtuoso } from "react-virtuoso"
// Basic list — dead simple API:
function SimpleList({ items }: { items: string[] }) {
return (
<Virtuoso
style={{ height: 400 }}
data={items}
itemContent={(index, item) => <div>{item}</div>}
/>
)
}
// Grouped list with sticky headers:
function GroupedList() {
const groupCounts = [10, 20, 15] // Items per group
const groupContent = (index: number) => (
<div style={{ background: "#eee", padding: "8px" }}>Group {index}</div>
)
return (
<GroupedVirtuoso
groupCounts={groupCounts}
groupContent={groupContent}
itemContent={(index) => <div>Item {index}</div>}
/>
)
}
// Infinite scroll built-in:
function InfiniteList({ loadMore }: { loadMore: () => void }) {
const [items, setItems] = useState<string[]>([...initial])
return (
<Virtuoso
style={{ height: 400 }}
data={items}
endReached={loadMore}
itemContent={(index, item) => <div>{item}</div>}
components={{
Footer: () => <div>Loading...</div>
}}
/>
)
}
// Virtual table (headless table cells + virtualization):
function VirtualTable({ rows, columns }) {
return (
<TableVirtuoso
data={rows}
fixedHeaderContent={() => (
<tr>{columns.map(col => <th key={col.key}>{col.label}</th>)}</tr>
)}
itemContent={(index, row) => (
columns.map(col => <td key={col.key}>{row[col.key]}</td>)
)}
/>
)
}
react-virtuoso built-in features:
- Dynamic item heights — auto-measured, no configuration
- Sticky group headers —
GroupedVirtuosocomponent - Top/bottom loading (bi-directional infinite scroll)
- Prepend items without scroll jump (e.g., chat history loading)
- ResizeObserver-based — handles item height changes after render
- First-render optimization — renders above-the-fold immediately
TableVirtuoso— virtual table with fixed headersScrollSeekPlaceholder— show placeholder during fast scroll
Feature Comparison
| Feature | @tanstack/react-virtual | react-virtuoso | react-window |
|---|---|---|---|
| Dynamic heights | ✅ measureElement | ✅ Auto | ⚠️ Manual |
| Sticky headers/groups | ❌ DIY | ✅ Built-in | ❌ |
| Infinite scroll | ❌ DIY | ✅ endReached | ❌ |
| Horizontal virtualization | ✅ | ✅ | ✅ |
| Grid virtualization | ✅ | ❌ | ✅ (FixedSizeGrid) |
| Virtual table | ❌ DIY | ✅ TableVirtuoso | ❌ |
| Bi-directional loading | ❌ | ✅ | ❌ |
| Bundle size | ~5KB | ~17KB | ~6KB |
| Framework agnostic | ✅ Core package | ❌ React only | ❌ React only |
| TypeScript | ✅ First-class | ✅ First-class | ⚠️ Types via @types |
| Active maintenance | ✅ | ✅ | ⚠️ Maintenance mode |
The bundle size column deserves context. The 5KB vs 17KB comparison is for the virtualization logic alone. In practice, @tanstack/react-virtual's lower bundle cost is partially offset by the rendering code you write yourself. React-virtuoso's higher baseline includes features — sticky headers, infinite scroll, grouped list support — that you would otherwise implement manually when using @tanstack/react-virtual. For applications that need those features, the "smaller" option often ends up with more total bytes when you factor in the custom implementation code.
The "Framework agnostic" row for @tanstack/react-virtual is practically significant for teams maintaining hybrid React/Vue or React/Svelte codebases. One shared virtualization implementation with framework-specific bindings is meaningfully simpler to maintain than two separate library integrations with different APIs and documentation. The same core virtualizer instance can power list rendering across all your framework surfaces, with only the rendering layer (the part that actually outputs DOM) being framework-specific.
Performance Characteristics
For a list of 100,000 items:
| Library | Initial Render | Scroll FPS | Memory (DOM nodes) |
|---|---|---|---|
| No virtualization | ~2000ms | ~15 FPS | 100,000 |
| react-window | ~8ms | 60 FPS | ~20 |
| @tanstack/react-virtual | ~6ms | 60 FPS | ~20 + overscan |
| react-virtuoso | ~12ms | 60 FPS | ~20 + buffer |
react-virtuoso has slightly more overhead due to its ResizeObserver usage and richer feature set, but the difference is imperceptible in normal usage.
When to Use Each
Choose @tanstack/react-virtual if:
- You want maximum control with a headless, composable API
- You're already using TanStack Query or TanStack Table
- You need non-React support (Vue, Solid, Svelte) via the core
- You're building drag-and-drop sortable virtual lists (pairs with @dnd-kit)
- You want to avoid the 17KB cost of react-virtuoso
Choose react-virtuoso if:
- You need dynamic item heights without measurement boilerplate
- You're building grouped lists with sticky headers
- You need infinite scroll (top, bottom, or both)
- You're virtualizing a table with fixed headers
- You want the richest API without DIY
Choose react-window if:
- You have an existing react-window implementation that works
- All your items are fixed height (the one thing react-window excels at)
- Absolute minimum bundle size is critical (~6KB vs 17KB)
The "choose react-window only for maintenance" guidance reflects a broader industry pattern: virtualization is now a solved problem, but the solutions have shifted from low-level utilities to higher-level components. React-window's minimal API was a feature when dynamic height support was hard; today, react-virtuoso and @tanstack/react-virtual both handle dynamic heights natively, making react-window's limitations a cost without a compensating benefit.
A practical consideration for teams choosing between react-virtuoso and @tanstack/react-virtual: accessibility. Both libraries render real DOM elements that can receive focus, but keyboard navigation for virtual lists requires explicit implementation. React-virtuoso's <Virtuoso> component accepts standard ARIA attributes and handles focus management for simple cases. @tanstack/react-virtual, being headless, requires you to implement focus management yourself using the scrollToIndex imperative API. For applications subject to WCAG 2.1 compliance requirements, react-virtuoso's head start on accessibility is meaningful.
For applications building virtualized data tables, the combination of @tanstack/react-table with @tanstack/react-virtual is the community-endorsed pattern. TanStack Table handles column logic, sorting, filtering, and row selection; @tanstack/react-virtual handles rendering only the visible rows. This pattern supports tables with tens of thousands of rows without pagination.
Real-World Usage Patterns
Chat Interface (react-virtuoso)
// Bi-directional infinite scroll with prepend-on-load (no scroll jump):
function ChatMessages({ channelId }: { channelId: string }) {
const [messages, setMessages] = useState(initialMessages)
const virtuosoRef = useRef<VirtuosoHandle>(null)
const loadOlder = async () => {
const older = await fetchOlderMessages(messages[0].id)
// Virtuoso handles scroll position preservation on prepend
setMessages([...older, ...messages])
}
return (
<Virtuoso
ref={virtuosoRef}
style={{ height: "100%" }}
data={messages}
startReached={loadOlder} // Load older messages at top
followOutput="smooth" // Auto-scroll for new messages
itemContent={(_, msg) => <ChatMessage message={msg} />}
initialTopMostItemIndex={messages.length - 1} // Start at bottom
/>
)
}
Data Table (TanStack Virtual + TanStack Table)
// @tanstack/react-virtual pairs naturally with @tanstack/react-table:
function VirtualDataTable({ data }: { data: Row[] }) {
const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() })
const { rows } = table.getRowModel()
const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 35,
overscan: 10,
})
return (
<div ref={parentRef} style={{ height: 500, overflow: "auto" }}>
<table>
<thead>{/* TanStack Table headers */}</thead>
<tbody style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map((virtualRow) => {
const row = rows[virtualRow.index]
return (
<tr
key={row.id}
ref={virtualizer.measureElement}
data-index={virtualRow.index}
style={{ transform: `translateY(${virtualRow.start}px)` }}
>
{row.getVisibleCells().map(cell => (
<td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>
))}
</tr>
)
})}
</tbody>
</table>
</div>
)
}
Ecosystem & Community
The virtualization landscape in React has consolidated significantly since 2022. React-window's dominance has waned as its fixed-size limitations became a real constraint in modern applications that display rich, dynamic content. By early 2026, react-virtuoso has overtaken react-window in weekly downloads, which tells a clear story: developers are willing to pay a modest bundle size premium for a library that handles real-world UI requirements without workarounds.
The TanStack ecosystem is one of the strongest in the React world. If your app already depends on TanStack Query for data fetching or TanStack Table for structured data display, adding @tanstack/react-virtual is a natural extension. The shared mental model of headless, hook-based primitives means your team won't need to context-switch between different API philosophies. The ecosystem also benefits from TanStack's active stewardship by Tanner Linsley, who has demonstrated long-term commitment to these libraries.
React-virtuoso benefits from a focused maintainer (Petar Bojinov) who has consistently delivered features that respond to community requests. Its GitHub discussions are active, the changelog is detailed, and the library has been adopted in notable open-source projects including parts of Rocket.Chat and several headless CMS admin panels. The virtuoso community on Discord is smaller than TanStack's but highly responsive.
React-window, while still downloaded nearly 1.9M times weekly due to existing projects, receives essentially no new development. The maintainer has been transparent about this. For greenfield development it is not a recommended choice, but it remains a safe choice if you're maintaining an existing codebase where it already works.
Real-World Adoption
React-virtuoso is the virtualization library of choice for several prominent open-source applications. Rocket.Chat uses it for their message list — the most demanding virtualization use case in existence, requiring bi-directional loading, dynamic message heights, and sticky date separators simultaneously. This makes it an excellent proof-of-concept for react-virtuoso's production reliability.
TanStack Virtual is used in several high-profile data grid implementations. The combination of @tanstack/react-table with @tanstack/react-virtual for virtualized rows is the recommended pattern in TanStack's own documentation, and it has become the default for enterprise React applications that need to display thousands of structured data rows. Companies building internal tooling platforms frequently reach for this combination.
React-window's production heritage is extensive — it was the standard from 2019 through 2022, meaning it's embedded in a massive number of production codebases at companies that built React applications during that era. Migration away from react-window is common but not urgent for teams that have it working correctly.
Developer Experience Deep Dive
TypeScript support across all three libraries is excellent in 2026, but with meaningful differences in ergonomics. React-virtuoso ships complete TypeScript definitions and the VirtuosoHandle ref type gives you full type safety when calling imperative methods like scrollToIndex. The component props are well-typed and IDE autocomplete works naturally.
@tanstack/react-virtual was designed TypeScript-first. The useVirtualizer hook returns a strongly-typed Virtualizer instance, and the generic parameter lets you type your scroll container. The framework-agnostic core package also has excellent TypeScript coverage, which is notable for a library designed to work across multiple frameworks.
React-window's TypeScript support comes through @types/react-window — a separate community-maintained package. While it works, it reflects the library's age and the fact that TypeScript wasn't a primary design consideration.
Debugging virtualization issues can be tricky. React-virtuoso's documentation includes a dedicated troubleshooting section covering common pitfalls like incorrect container heights and ResizeObserver timing issues. @tanstack/react-virtual has excellent community resources on Stack Overflow and GitHub discussions. Both libraries work well with React DevTools.
Documentation quality: react-virtuoso wins with an interactive demo site that lets you toggle between different configurations in real time. TanStack Virtual's documentation is part of the broader TanStack docs site, which is comprehensive but requires more navigation. React-window's docs are minimal but the library is so focused that minimal docs suffice.
Performance & Benchmarks
The performance of all three libraries is excellent for typical use cases — the key insight is that once you're virtualizing, the bottleneck shifts from DOM node count to your item renderer complexity. A complex card component that triggers expensive re-renders will overwhelm any virtualization library regardless of which one you choose.
For pure scroll performance with simple item renderers and 100,000 items, all three achieve 60 FPS on modern hardware. The differences emerge in specific scenarios. React-window is fastest for fixed-size lists because it requires no measurement overhead — all positions are mathematically calculated upfront. This advantage vanishes the moment you need variable heights.
@tanstack/react-virtual's measureElement approach introduces a two-pass render for each new item (estimate → render → measure → recalculate), which means items can "jump" slightly when they first appear if your size estimates are far off. Setting a reasonable estimateSize value — close to the average real height — minimizes this effect. With good estimates, the visual experience is indistinguishable from pre-measured heights.
React-virtuoso uses a ResizeObserver approach that's slightly more expensive initially but handles dynamic height changes (content expanding/collapsing, images loading, text reflowing) without any manual intervention. For chat interfaces and social feeds where content height changes after render, this is the correct trade-off.
Memory usage across all three is similar — the virtual DOM kept in memory for overscan items is small compared to other React application state.
Migration Guide
Migrating from react-window to react-virtuoso:
The conceptual model shift is from "render function receives style prop" to "data-driven component with itemContent." The biggest practical change is that you stop needing to calculate item heights. Your item components can be any height and react-virtuoso handles the rest automatically. Expect to simplify your code considerably when making this migration — most react-window projects have accumulated workarounds for dynamic heights that can be deleted entirely.
Migrating from react-window to @tanstack/react-virtual:
The change requires rethinking the rendering structure. React-window wraps your render function and provides the positioning style prop. @tanstack/react-virtual is headless — you render the outer container, the inner height container, and each item using transform: translateY positioning yourself. This gives more control but requires understanding the absolute positioning model. The official docs include a migration guide with before/after examples.
Migrating from react-virtuoso to @tanstack/react-virtual (or vice versa):
This is less common since they serve different audiences, but the main consideration is that react-virtuoso provides components while @tanstack/react-virtual provides a hook. If you're moving to TanStack Virtual because you need framework-agnostic code or are integrating with TanStack Table, plan to rebuild your rendering layer from scratch — the mental model is different enough that incremental migration isn't practical.
Common pitfalls regardless of library: forgetting to give the scroll container an explicit height, not memoizing item components (causing all visible items to re-render on any state change), and not handling keyboard navigation correctly for accessibility.
Final Verdict 2026
The virtualization library landscape has reached a comfortable equilibrium. React-window is not a good choice for new projects but remains valid for maintained codebases. The real decision is between react-virtuoso and @tanstack/react-virtual.
Choose react-virtuoso when you're building for specific use cases: chat interfaces, social feeds, grouped/categorized lists, infinite scroll in any direction, or virtualized tables. The 17KB bundle cost pays for itself immediately in the features you won't have to build.
Choose @tanstack/react-virtual when you're already in the TanStack ecosystem, need the absolute minimum runtime overhead, want to target multiple frameworks, or are building custom scroll behaviors that don't fit react-virtuoso's component model. The headless API gives you complete control.
For the majority of React applications displaying long lists in 2026, react-virtuoso is the pragmatic default. Its out-of-the-box capabilities cover the most common virtualization requirements, and its documentation makes the learning curve gentle.
Methodology
Download data from npm registry (weekly average, February 2026). Bundle sizes from bundlephobia. Performance measurements are approximations based on community benchmarks with 100K uniform string items.
Compare virtual list packages on PkgPulse →
Related:
Best React Component Libraries 2026