Virtua React: Build High-Performance Virtual Lists Without Losing Your Mind
React virtual list
VList
Virtualizer
React scroll performance
virtua installation
React large list rendering
windowing
react-window alternative
The DOM Doesn’t Care How Hard You Worked on That List
You’ve built a beautiful React list component. It renders product cards, log entries, user rows — whatever the business needs. And it works fine. Right up until the moment someone loads 10,000 items into it, at which point the browser takes a long, hard look at your work and decides to render all 10,000 DOM nodes simultaneously. Frame rate craters. Scrolling stutters. Users leave. The DOM doesn’t care about your feelings.
This is the classic React large list rendering problem, and it’s older than most of the developers currently solving it. The browser can only paint what’s visible in the viewport — but without intervention, React will happily construct a full DOM tree for every single item in your dataset. With a few hundred items, you won’t notice. With thousands, you’re looking at seconds of blocking render time, bloated memory usage, and the kind of scroll performance that makes users question your entire professional career.
The solution is virtualization — also called “windowing.” Instead of mounting every list item, you render only the items currently visible (plus a small buffer), and swap them in and out as the user scrolls. The DOM stays lean, the frame rate stays high, and your users stay on the page. The concept is simple. The implementation, historically, has been… less so. That’s where Virtua enters the picture.
What Is Virtua and Why Should You Care?
Virtua is a modern, zero-config React virtualization library built for the way developers actually write React today. It was created to address the friction that comes with older tools like react-window and react-virtualized — libraries that remain functional but carry conceptual baggage from a pre-hooks, class-component era. Virtua takes a different stance: give you powerful virtualization with minimal configuration and maximum composability.
What makes Virtua stand out in a crowded field of React list component solutions is its automatic item measurement. Where react-window requires you to specify itemSize as a fixed number or a function, Virtua uses ResizeObserver under the hood to measure items after they render. This means you get accurate scrollbar behavior, correct positioning, and smooth scroll performance — even with variable-height items — without writing a single measurement utility. That’s not a small thing. Dynamic content is the norm, not the exception.
Virtua also ships with full TypeScript support, React 18 compatibility (including Concurrent Mode), and a tiny footprint. It plays nicely with SSR setups, supports horizontal lists, and exposes a low-level Virtualizer primitive for cases where you need to plug virtualization into an existing layout rather than wrap everything in a new scroll container. It’s the kind of library that feels like it was written by someone who actually had to ship production apps with virtualized lists — because it was.
Virtua Installation: Up and Running in 60 Seconds
Virtua installation is refreshingly boring, which is exactly what you want from a dependency. Open your terminal, navigate to your React project, and run one of the following:
# npm
npm install virtua
# yarn
yarn add virtua
# pnpm
pnpm add virtua
That’s it. No peer dependency gymnastics, no polyfill configuration, no craco workarounds. Virtua targets modern browsers that support ResizeObserver natively (which, at this point, is essentially everything except IE — and if you’re still supporting IE in 2025, virtualization is the least of your problems). For the rare case where ResizeObserver isn’t available, a polyfill like @juggle/resize-observer drops in without issue.
After installation, you have access to two primary exports: VList and Virtualizer. A third export, WVList, handles windowed lists for cases where the scroll container is the window itself rather than a fixed-height div — useful for page-level infinite scroll implementations. For most use cases, you’ll reach for VList first and graduate to Virtualizer when you need more control. Let’s look at both.
VList: The Component You’ll Use 90% of the Time
Virtua VList is the primary high-level component — a self-contained, scrollable, virtualized list. It manages its own scroll container, measures its children automatically, and handles all the viewport math internally. You pass it children, give it a height, and it takes care of everything else. Here’s the most basic possible usage:
import { VList } from "virtua";
const items = Array.from({ length: 10000 }, (_, i) => `Item ${i + 1}`);
export default function BasicList() {
return (
<VList style={{ height: "600px" }}>
{items.map((item) => (
<div key={item} style={{ padding: "12px 16px", borderBottom: "1px solid #e5e7eb" }}>
{item}
</div>
))}
</VList>
);
}
That renders a virtualized list of 10,000 items where, at any given moment, only the visible ones (plus a small overscan buffer) exist in the DOM. No itemSize prop. No row height calculation. No wrapper component gymnastics. You write your list items exactly as you would without virtualization, and Virtua handles the rest. The scroll performance on this is, frankly, embarrassingly good — 60fps on hardware that would make a non-virtualized version crawl.
VList also supports a handful of useful props for real-world use cases. overscan controls how many items outside the viewport are kept mounted (default is 4). onScroll fires during scrolling with the current scroll offset. onRangeChange gives you the currently visible index range, which is perfect for implementing “you’re viewing items 150–175 of 10,000” indicators. And ref exposes an imperative handle with a scrollToIndex() method — essential for programmatic navigation like “jump to item” features.
import { useRef } from "react";
import { VList, VListHandle } from "virtua";
export default function ListWithJump() {
const ref = useRef<VListHandle>(null);
return (
<>
<button onClick={() => ref.current?.scrollToIndex(999, { align: "center" })}>
Jump to item 1000
</button>
<VList ref={ref} style={{ height: "500px" }}>
{Array.from({ length: 5000 }, (_, i) => (
<div key={i} style={{ padding: "10px" }}>Row {i + 1}</div>
))}
</VList>
<>;
);
}
Virtualizer: When You Need to Own the Scroll Container
Virtua Virtualizer is the lower-level primitive that powers VList internally. The key difference is ownership of the scroll container: VList creates its own; Virtualizer attaches to one you provide. This matters whenever you’re working with an existing layout where the scroll container is already defined — a sidebar with overflow-y: auto, a modal body, a native mobile scroll element in React Native Web, or a window-level scroller.
import { useRef } from "react";
import { Virtualizer } from "virtua";
export default function CustomScrollList() {
const scrollerRef = useRef<HTMLDivElement>(null);
return (
<div
ref={scrollerRef}
style={{ height: "600px", overflowY: "auto", border: "1px solid #ddd" }}
>
<Virtualizer scrollRef={scrollerRef}>
{Array.from({ length: 8000 }, (_, i) => (
<div key={i} style={{ padding: "12px", borderBottom: "1px solid #eee" }}>
Dynamic height row {i + 1} — {Math.random().toString(36).slice(2, 10)}
</div>
))}
</Virtualizer>
</div>
);
}
Notice the pattern: you create a scrollable div with a ref, pass that ref to Virtualizer via the scrollRef prop, and nest your items inside. Virtualizer observes the scroll container, measures items with ResizeObserver, and manages which children are in the DOM. Your scroll container CSS is untouched — you keep full control over borders, padding, background, overflow behavior, and anything else. This composability is where Virtua genuinely shines compared to more opinionated libraries.
Virtualizer also supports horizontal virtualization via the horizontal prop, making it suitable for carousels, timeline views, and data grids where you need to scroll columns rather than rows. Combined with VList‘s horizontal mode, Virtua covers the vast majority of React scroll performance scenarios without needing a separate library for each layout direction. For two-dimensional grids, you’d combine nested Virtualizer instances — a pattern that, while more involved, is fully documented and well-supported.
A Real Virtua Example: Infinite Scroll with Dynamic Content
Abstract examples are fine for learning syntax. Real applications involve async data, variable content sizes, loading states, and the ever-present stakeholder request to “make it feel like Twitter.” Here’s a more complete virtua example — a feed-style list that fetches pages of data as the user scrolls, handles variable item heights gracefully, and shows a loading indicator at the bottom:
import { useState, useCallback } from "react";
import { VList } from "virtua";
interface Post {
id: number;
text: string;
author: string;
}
async function fetchPosts(page: number): Promise<Post[]> {
// Replace with your actual API call
return Array.from({ length: 20 }, (_, i) => ({
id: page * 20 + i,
author: `User ${Math.floor(Math.random() * 100)}`,
text: "Lorem ipsum ".repeat(Math.ceil(Math.random() * 8)).trim(),
}));
}
export default function InfiniteFeed() {
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(0);
const loadMore = useCallback(async () => {
if (loading) return;
setLoading(true);
const newPosts = await fetchPosts(page);
setPosts((prev) => [...prev, ...newPosts]);
setPage((p) => p + 1);
setLoading(false);
}, [loading, page]);
const handleRangeChange = useCallback(
(startIndex: number, endIndex: number) => {
// Trigger load when user is near the bottom
if (endIndex >= posts.length - 5 && !loading) {
loadMore();
}
},
[posts.length, loading, loadMore]
);
return (
<VList
style={{ height: "80vh", maxWidth: "680px", margin: "0 auto" }}
onRangeChange={handleRangeChange}
>
{posts.map((post) => (
<article
key={post.id}
style={{ padding: "16px", borderBottom: "1px solid #e5e7eb" }}
>
<strong>{post.author}</strong>
<p style={{ margin: "6px 0 0" }}>{post.text}</p>
</article>
))}
{loading && (
<div style={{ padding: "16px", textAlign: "center", color: "#9ca3af" }}>
Loading more…
</div>
)}
</VList>
);
}
The key detail here is onRangeChange. This callback fires with the current visible index range on every scroll event. When endIndex gets within 5 items of the total post count, we trigger loadMore(). Because Virtua handles variable heights automatically, the loading indicator at the bottom (which has a different height than a post card) doesn’t require any special handling — it just works. This is the kind of thing that would require careful estimatedItemSize tuning in react-window; with Virtua, you write the natural React you’d write anyway.
One important architectural note: if you’re using React 18’s useTransition or useDeferredValue for rendering large datasets, Virtua plays well with Concurrent Mode. Items that are deferred or suspended (with Suspense boundaries per item) are handled gracefully — Virtua will measure the fallback height initially and reflow when the real content resolves. This makes it one of the few React list components that fully embraces the React 18 rendering model rather than working around it.
React Performance Optimization: What Virtua Does Under the Hood
Understanding why Virtua performs well — not just that it does — makes you a better consumer of the library and helps you avoid the subtle footguns that can undercut virtualization gains. The core mechanism is viewport-based rendering: Virtua maintains an internal model of item positions and sizes, calculates which indices fall within the visible viewport (plus overscan), and renders only those items as React children. Items outside the range are unmounted. Positioning is handled via absolute CSS placement within a relatively-positioned container, so the scrollbar reflects the total height of all items even though most aren’t in the DOM.
The size tracking uses ResizeObserver per item. When a new item mounts, Virtua attaches an observer and reads its contentRect. On resize (say, because a user expanded a comment or the font loaded), the observer fires, the internal position model updates, and the layout reflows. This happens outside React’s render cycle, which means it’s non-blocking. Compare this to the alternative of running measurements in useLayoutEffect — synchronous, blocking, and an easy way to create jank spikes in large lists.
To get the most out of Virtua for React performance optimization, apply standard React memoization practices to your item components. Wrap individual item renderers in React.memo so that a scroll event (which re-renders the parent) doesn’t cascade into re-renders for items that haven’t changed data. Pair this with stable key props (use your data IDs, never array indices for dynamic lists) and you’ll hit the ceiling of what JavaScript can do for list rendering without moving to a canvas-based approach.
VList children. Extract them as named components wrapped in React.memo. An inline arrow function creates a new reference on every parent render, defeating memoization entirely and causing every visible item to re-render on each scroll tick.
Virtua vs. react-window vs. react-virtualized: An Honest Comparison
The honest answer to “which React virtualization library should I use” is: it depends on what you’re building, but Virtua deserves to be your first instinct in 2025. Here’s a clear-eyed comparison of the three most common options:
| Feature | Virtua | react-window | react-virtualized |
|---|---|---|---|
| Auto item measurement | ✅ Yes (ResizeObserver) | ❌ No (manual itemSize) | ⚠️ CellMeasurer (complex) |
| Variable height support | ✅ Native | ⚠️ Workaround needed | ✅ Via CellMeasurer |
| React 18 / Concurrent Mode | ✅ Full support | ⚠️ Partial | ❌ Limited |
| Bundle size (approx.) | ~8KB gzipped | ~6KB gzipped | ~30KB gzipped |
| TypeScript-first | ✅ Yes | ✅ Yes | ⚠️ Partial |
| Horizontal lists | ✅ Yes | ✅ Yes | ✅ Yes |
| Window scroll support | ✅ WVList | ❌ No | ⚠️ WindowScroller |
| Active maintenance | ✅ Active (2024–25) | ⚠️ Minimal | ⚠️ Mostly stable/stale |
react-window remains a legitimate choice if you have fixed-height items and want the smallest possible bundle. Its mental model is clean and its performance ceiling is high for that specific use case. But “fixed-height items” describes a minority of real UI work. The moment your list contains cards with expandable sections, user-generated text, or images of variable dimensions, you’re writing measurement utilities that should have been the library’s job.
react-virtualized solved the variable-height problem with CellMeasurer, but the API surface became enormous, the bundle ballooned, and React 18 compatibility has been inconsistent. It’s a library that did tremendous work establishing patterns for the entire ecosystem — and one you should probably migrate off if you’re starting a new project today. Virtua inherits the best ideas from both predecessors, removes the configuration overhead, and ships with the React 18 rendering model as a first-class concern rather than an afterthought.
Virtua Setup Patterns for Production Applications
Getting Virtua into a demo is trivial; getting it into a production application involves a few more considerations. The first is SSR compatibility. Virtua’s size measurement relies on ResizeObserver, which doesn’t exist in Node environments. Virtua handles this gracefully — on the server, it renders a placeholder structure that hydrates correctly on the client. However, if you’re using Next.js App Router with Server Components, make sure your virtualized list component is marked "use client", because ResizeObserver callbacks and scroll event listeners are client-side concerns by definition.
The second production consideration is scroll restoration. When a user navigates away from a virtualized list and returns (back button, React Router navigation), you typically want to restore their scroll position. VList‘s imperative ref exposes scrollOffset for reading the current position and scrollTo({ offset }) for restoring it. Store the offset in your router state or a sessionStorage key, read it on component mount, and call scrollTo inside a useLayoutEffect. This gives you native-feeling scroll restoration without the browser’s default behavior fighting your virtualized layout.
Third: accessibility. Virtualized lists can confuse screen readers because the DOM doesn’t represent the full dataset. Virtua doesn’t solve this automatically — no virtualization library does, because it’s an application-level concern. The recommended approach is to add appropriate ARIA attributes: role="list" on the VList container (via the as prop or a wrapper), role="listitem" on each item, and aria-setsize / aria-posinset on items when the total count is known. For keyboard navigation within the list, intercept Home / End keys and call scrollToIndex(0) and scrollToIndex(items.length - 1) respectively.
Beyond Vertical: Horizontal Lists and Grid Layouts
The virtua tutorial landscape often focuses exclusively on vertical lists, but Virtua’s horizontal mode is equally capable. Enabling it is a single prop: <VList horizontal>. The component switches to horizontal scrolling, measures item widths instead of heights, and handles all the same auto-measurement magic in the horizontal axis. This is perfect for media carousels, horizontal timelines, or tab-scrolling navigation where the dataset is too large to render all at once.
import { VList } from "virtua";
const images = Array.from({ length: 500 }, (_, i) => ({
id: i,
src: `https://picsum.photos/seed/${i}/200/150`,
alt: `Photo ${i + 1}`,
}));
export default function HorizontalCarousel() {
return (
<VList horizontal style={{ height: "180px", width: "100%" }}>
{images.map((img) => (
<div key={img.id} style={{ padding: "8px", flexShrink: 0 }}>
<img
src={img.src}
alt={img.alt}
style={{ height: "150px", borderRadius: "8px", display: "block" }}
/>
</div>
))}
</VList>
);
}
For true two-dimensional grid virtualization — think a spreadsheet or an image grid — Virtua’s recommended pattern is nesting: a vertical Virtualizer for rows, each row containing a horizontal Virtualizer for cells. This requires the outer scroller to be the window or a defined container, and the inner scrollers to be purely layout-driven (not independently scrollable). It’s a more advanced setup and the Virtua docs walk through it in detail, but the composable primitives make it substantially less painful than trying to configure the equivalent in react-virtualized‘s Grid component.
One pattern worth calling out for data-heavy applications is using Virtualizer inside a table-like structure. Because Virtualizer lets you own the scroll container, you can place it inside a <tbody> and virtualize table rows without breaking semantic HTML. Virtua renders real DOM elements as its item containers (configurable via the as prop), so you can have <Virtualizer as="tbody"> with <tr> children and maintain a valid, accessible table structure while still only mounting visible rows. This is one of those details that separates “demo library” from “production-ready toolkit.”
Frequently Asked Questions
How do I install and set up Virtua in a React project?
Run npm install virtua (or yarn add virtua / pnpm add virtua) in your project directory. No additional build configuration is required. Import VList or Virtualizer directly from 'virtua' and wrap your list items. For SSR environments like Next.js App Router, mark the consuming component with "use client" at the top of the file.
What is the difference between VList and Virtualizer in Virtua?
VList is a complete, self-contained scrollable list component — it creates and manages its own scroll container. You give it a height via CSS and drop your items inside. Virtualizer is the underlying primitive that attaches virtualization to a scroll container you control via a scrollRef prop. Use VList for standalone lists; use Virtualizer when you need to virtualize content inside an existing scrollable element like a modal body, sidebar, or the browser window itself.
Can Virtua handle dynamic or variable item heights?
Yes — this is one of Virtua’s core strengths. It uses ResizeObserver to measure each item’s actual rendered size after it mounts. You don’t need to provide an itemSize prop, an estimatedItemSize, or a measurement utility function. Items with different heights (cards, expanded rows, posts with images) are measured and positioned automatically. If an item’s size changes after the initial render (e.g., a user expands a section), Virtua detects the change and reflows the layout accordingly.