← Back to blog
··13 min read

TanStack Virtual vs react-window vs react-virtuoso 2026: The Best React List Virtualization Library

TanStack Virtualreact-windowreact-virtuosoVirtualizationReactPerformanceTypeScriptNext.js
TanStack Virtual vs react-window vs react-virtuoso 2026: The Best React List Virtualization Library

# TanStack Virtual vs react-window vs react-virtuoso 2026: The Best React List Virtualization Library

Sooner or later every serious app hits the same wall: a list that is too long to render. A data table with ten thousand rows, an infinite feed, a chat log, a search result set — mount all of it into the DOM and the browser chokes. The fix is list virtualization: render only what is on screen, and swap content as the user scrolls. In 2026 three libraries own this problem in React — TanStack Virtual, react-window, and react-virtuoso — and they take three very different philosophies. This guide compares them on the things that actually decide the choice.

If you build or sell production-ready templates, a smooth ten-thousand-row table is exactly the kind of quiet quality a buyer notices in the first ten seconds.

The 30-Second Answer

  • react-window — the smallest and simplest (~2–3 KB). Fixed or computable variable sizes, lists and grids, from the author of react-virtualized. The right pick when your rows are uniform and you want minimal footprint.
  • TanStack Virtual — headless and framework-agnostic (~3–4 KB, zero deps). A hook returns the virtual items and their positions; *you* render the markup. Full control, dynamic measured heights, horizontal and grid virtualization, and a natural pairing with TanStack Table.
  • react-virtuoso — batteries-included (largest bundle). Automatically measures variable and unknown row heights and ships infinite scroll, grouped sticky headers, tables, grids, and reverse chat scroll out of the box. The fastest way to ship a hard case correctly.
  • The Landscape: Three Philosophies

    These libraries are not really competing on the same axis. They are three answers to one question: *how much do you want to do yourself, and how much should the library handle?*

    react-window is deliberately minimal. You give it a row count and a size, it gives you a scroll container with absolutely positioned rows. It knows nothing about your content — which keeps it tiny and fast, and puts the burden of knowing row sizes on you.

    TanStack Virtual is *headless*. It computes which items are visible and where each one sits, and hands you that as data through the useVirtualizer hook. You render every element yourself. It imposes no markup or styling, so it fits any layout — and because it is from the TanStack team behind TanStack Query and Table, it slots into that ecosystem cleanly.

    react-virtuoso is the opposite bet: a complete component that measures rows for you and solves the hard cases (unknown heights, grouped lists, infinite scroll, chat) with almost no code. You give up bundle size and low-level control in exchange for shipping fast.

    Bundle Size and Downloads

    Size is the most visible difference, and it tracks the philosophy.

    LibraryGzipped sizeWeekly downloadsStyle
    **react-window**~2–3 KB~3MComponents, manual sizes
    **TanStack Virtual**~3–4 KB~4MHeadless hook
    **react-virtuoso**~14 KB~2MFull component, auto-measure

    react-window and TanStack Virtual are both small enough that size rarely decides against them. react-virtuoso is several times larger — a fair trade when it saves you writing a measurement and infinite-scroll layer by hand, and a poor one when your rows are uniform and simple.

    react-window: Minimal and Manual

    react-window is what you reach for when your rows are uniform and you want the least possible code and bytes. It comes from Brian Vaughn, who also wrote the older, heavier react-virtualized — this is the focused successor.

    tsx
    import { FixedSizeList } from "react-window";
    
    function Rows({ items }: { items: string[] }) {
      return (
        <FixedSizeList
          height={600}
          width="100%"
          itemCount={items.length}
          itemSize={48} // every row is 48px
        >
          {({ index, style }) => (
            // style positions the row — you MUST spread it
            <div style={style}>{items[index]}</div>
          )}
        </FixedSizeList>
      );
    }

    What you get: FixedSizeList, VariableSizeList, and grid equivalents, in a couple of kilobytes. What it costs: you must know your sizes. VariableSizeList takes an itemSize(index) function, which is perfect for *computable* heights but awkward for content-driven ones (text of unknown length, images that load late) — those usually need a separate measurement library bolted on.

    Use react-window when: rows are uniform or you can compute their heights, and you want the smallest, most stable option. It is a clear upgrade over react-virtualized for new code.

    TanStack Virtual: Headless and Flexible

    TanStack Virtual gives you a hook, not a component. It tells you which items are virtual-visible and where to place them; you own the DOM entirely.

    tsx
    import { useRef } from "react";
    import { useVirtualizer } from "@tanstack/react-virtual";
    
    function Rows({ items }: { items: string[] }) {
      const parentRef = useRef<HTMLDivElement>(null);
    
      const virtualizer = useVirtualizer({
        count: items.length,
        getScrollElement: () => parentRef.current,
        estimateSize: () => 48, // starting estimate; measured for real below
      });
    
      return (
        <div ref={parentRef} style={{ height: 600, overflow: "auto" }}>
          <div
            style={{ height: virtualizer.getTotalSize(), position: "relative" }}
          >
            {virtualizer.getVirtualItems().map((row) => (
              <div
                key={row.key}
                ref={virtualizer.measureElement} // dynamic height measurement
                data-index={row.index}
                style={{
                  position: "absolute",
                  top: 0,
                  transform: `translateY(${row.start}px)`,
                  width: "100%",
                }}
              >
                {items[row.index]}
              </div>
            ))}
          </div>
        </div>
      );
    }

    What you get: total control over markup and styling, vertical/horizontal/grid virtualization, dynamic heights via measureElement, and the same API across React, Vue, Svelte, and Solid. Because it is headless, it drops straight into a custom design system or a TanStack Table without fighting anyone's opinions. What it costs: more boilerplate than the other two — the positioning wrapper and the measureElement ref are yours to wire up.

    Use TanStack Virtual when: you want a tiny footprint *and* dynamic heights or custom layouts, you are already in the TanStack ecosystem, or you need the same virtualizer across frameworks.

    react-virtuoso: Automatic and Complete

    react-virtuoso is the batteries-included choice. You do not tell it row sizes — it measures them for you, including when they change — and the hard features come built in.

    tsx
    import { Virtuoso } from "react-virtuoso";
    
    function Feed({ items, loadMore }: { items: Item[]; loadMore: () => void }) {
      return (
        <Virtuoso
          style={{ height: 600 }}
          data={items}
          endReached={loadMore} // infinite scroll, built in
          itemContent={(index, item) => (
            // rows can be any height — measured automatically
            <Card title={item.title}>{item.body}</Card>
          )}
        />
      );
    }

    What you get: automatic variable-height measurement (no size functions), built-in infinite scroll (endReached), grouped lists with sticky headers, a TableVirtuoso for real

    markup, a grid, and reverse scrolling for chat UIs. It handles the cases that make the other two more work. What it costs: ~14 KB and less low-level control — you render inside its structure rather than owning the whole DOM.

    Use react-virtuoso when: you have unknown row heights, need infinite scroll or grouped sticky headers or a chat view, and want to ship it correctly today.

    The Decision Table

    NeedBest pick
    Simple uniform rows, smallest bundle**react-window**
    Full control over markup / custom layout**TanStack Virtual**
    Dynamic heights, tiny footprint**TanStack Virtual**
    Dynamic heights, least code**react-virtuoso**
    Built-in infinite scroll**react-virtuoso**
    Grouped lists + sticky headers**react-virtuoso**
    Chat / reverse scroll**react-virtuoso**
    Horizontal or grid virtualization**TanStack Virtual**
    Pairing with TanStack Table**TanStack Virtual**
    Cross-framework (Vue/Svelte/Solid too)**TanStack Virtual**
    Upgrade from react-virtualized**react-window** or TanStack Virtual

    The Gotchas Nobody Warns You About

    A few things trip up almost everyone the first time, regardless of library:

  • The scroll container needs a fixed height. Virtualization measures against a scrollable element. If your container has no explicit height (or a height: 100% chain that never resolves), it collapses and renders one row or nothing.
  • Spread the style react-window gives you. Those inline styles do the absolute positioning. Drop them and every row stacks at the top.
  • Dynamic heights cause a first-paint shift. With measured heights (TanStack Virtual's measureElement, or react-virtuoso), rows render at an estimate, then snap to their real size. Use a realistic estimateSize to minimize the jump.
  • Sticky elements and virtualization fight. Sticky table headers or section headers need first-class support — this is exactly where react-virtuoso's grouped mode earns its bytes.
  • Keys must be stable. Key rows by a stable id, not the array index, or reordering and data changes will scramble measured heights.
  • How It Fits Your Stack

    Virtualization is a layer, not a silo. Under a big list you usually have a data-fetching layer paging rows in — see TanStack Query vs SWR vs RTK Query — and often a table library owning columns, sorting, and selection while the virtualizer only handles rendering: TanStack Table vs AG Grid vs MUI Data Grid covers that split (and TanStack Table + TanStack Virtual is a deliberate, well-worn pairing). For dashboards where all of this lands together, the best Next.js analytics dashboard templates show the pattern in context.

    Why This Matters if You Sell Templates

    A buyer opening your React dashboard or admin kit scrolls the biggest table first. If ten thousand rows glide at sixty frames per second, the code reads as *fast and professional*; if the tab freezes on load, no feature list saves the sale. Choosing the right virtualizer — and wiring it up without the layout shift and sticky-header bugs above — is the kind of polish that turns a browse into a purchase, and it sits naturally beside the production-ready quality buyers come to CodeCudos for.

    The Bottom Line

    All three render only what is on screen — but they hand you a very different amount of the work.

  • react-window — the minimalist: tiny and stable, ideal for uniform or computable rows, a clean upgrade from react-virtualized. Reach for it when simplicity and size win.
  • TanStack Virtual — the headless one: full control, dynamic heights, grid and horizontal support, cross-framework, and a natural fit with TanStack Table — for a bit more boilerplate. The modern default when you want flexibility with a small footprint.
  • react-virtuoso — the complete one: automatic measurement plus infinite scroll, grouped sticky headers, tables, and chat built in, at a larger bundle. The fastest way to ship the hard cases right.
  • Pick react-window for simple lists, TanStack Virtual when you want control and a tiny bundle, and react-virtuoso when you want the difficult cases solved for you — and remember that for a list of a dozen items, the right answer is no virtualization at all.

    Ready to turn what you build into income? List your React or Next.js template on CodeCudos, make the big tables smooth, and ship something that reads as production-ready from the first scroll.

    Frequently asked questions

    What is list virtualization and when do I actually need it?▾

    List virtualization (also called windowing) is the technique of rendering only the rows currently visible in the viewport plus a small buffer, instead of mounting every row in a long list into the DOM. If you render a table or feed with thousands of rows the naive way, the browser has to create, lay out, and paint thousands of DOM nodes, which blows up memory, makes the initial render slow, and causes janky scrolling and slow interactions. A virtualizer keeps a fixed-size window of nodes in the DOM and swaps their content and position as you scroll, so a list of ten thousand rows costs about the same as a list of twenty. You need it once a list is long enough that mounting all of it hurts — as a rough rule, a few hundred rows of non-trivial content or any list that can grow unbounded (search results, logs, chat, infinite feeds, large data tables). For a short, fixed list of a dozen items, virtualization adds complexity for no benefit and you should skip it. The three libraries in this comparison — TanStack Virtual, react-window, and react-virtuoso — are the modern React answers to this problem.

    What is the difference between TanStack Virtual, react-window, and react-virtuoso?▾

    The core difference is how much they do for you and how much control you keep. react-window is a small, focused set of components (FixedSizeList, VariableSizeList, and grid variants) from Brian Vaughn, the author of the older react-virtualized; you tell it the item count and the item size and it renders a scroll container with absolutely positioned rows. It is tiny and fast but expects you to know your row sizes, so truly dynamic content needs extra work. TanStack Virtual is headless: it is a hook (useVirtualizer) that computes which items are visible and where they should be positioned, and you render all the markup yourself. That means it imposes no styling or DOM structure, supports vertical, horizontal, and grid virtualization, handles dynamic measured heights via a measureElement callback, works across React, Vue, Svelte, and Solid, and pairs naturally with TanStack Table — at the cost of writing a bit more boilerplate. react-virtuoso is the opposite philosophy: a batteries-included component that measures every row automatically (no size functions needed), and ships infinite scroll, grouped lists with sticky headers, tables, grids, and reverse chat scrolling built in. It is the largest of the three but solves the hardest cases with the least code. In short: react-window is minimal and manual, TanStack Virtual is headless and flexible, react-virtuoso is automatic and complete.

    Which virtualization library is best for variable or dynamic row heights?▾

    For rows whose height you cannot know in advance — comments with different text lengths, cards with images that load late, chat messages — react-virtuoso is the easiest and most reliable, because automatic measurement is its core design: you render your rows and it measures and repositions them for you, including when their size changes after mount. TanStack Virtual also handles dynamic heights well through its measureElement API, where you attach a ref to each rendered row and the virtualizer measures the real DOM size and adjusts; it takes a little more code than react-virtuoso but gives you full control and a tiny bundle. react-window can do variable sizes with VariableSizeList, but you must supply the size of each item up front via an itemSize function, so it fits variable-but-known heights (rows you can compute) far better than truly unknown, content-driven heights, which typically require an extra measurement library or manual work. So the ranking for genuinely dynamic content is react-virtuoso for least effort, TanStack Virtual for control with a small footprint, and react-window only when you can compute the sizes yourself.

    Do these virtualization libraries work with Next.js and server-side rendering?▾

    All three work in a Next.js app, but virtualization is fundamentally a client-side, measurement-driven technique, so the practical pattern is the same across them: the virtualizer needs the real scroll container and row sizes from the browser, which do not exist during server rendering. In the Next.js App Router you render the virtualized list inside a Client Component (a file with the use client directive), and you generally want it to mount on the client rather than trying to server-render thousands of rows, which would defeat the purpose. TanStack Virtual and react-window both render fine this way and hydrate cleanly; react-virtuoso also supports SSR and even has options to render an initial item count on the server for content that must appear in the initial HTML for SEO or first paint. The key point for any of them is that the numbers only appear once the component measures the DOM, so avoid depending on virtualized rows for above-the-fold server-rendered content, and keep the list in a client boundary. For where this fits in a wider setup, see our guide on the best tech stack for web apps in 2026.

    Is react-window still maintained, and should I use it over react-virtualized?▾

    react-window was written by Brian Vaughn as a lighter, more focused replacement for his own earlier library react-virtualized, and for new projects you should prefer react-window (or one of the other two modern options) over react-virtualized, which is heavier and effectively legacy. react-window is small, stable, and battle-tested, and for the case it targets — lists and grids of fixed or computable size — it does the job in the fewest kilobytes. The caveat in 2026 is that it is a mature, slow-moving library rather than an actively expanding one, so if you need dynamic measured heights, grouped sticky headers, or built-in infinite scroll, you will either bolt on extra code or be better served by TanStack Virtual (headless and actively developed) or react-virtuoso (feature-complete). The honest summary: react-window is a great, tiny choice for simple uniform lists and a clear upgrade over react-virtualized, but for anything with unknown row sizes or richer behavior, reach for TanStack Virtual or react-virtuoso instead.

    Related guides

    Browse Quality-Scored Code

    Every listing on CodeCudos is analyzed for code quality, security, and documentation. Find production-ready components, templates, and apps — or sell your own code and keep 90%.

    Browse Marketplace →