Stop 16 · PerformanceTopics · 5/13

Virtualization

Part of the Frontend Developer Roadmap

In short

Rendering 10,000 rows makes the page crawl, especially on a budget phone. Virtualization draws only the 20 or so rows on screen and swaps them as you scroll.

Rendering only the visible portion of a very long list or table (plus a small buffer), rather than every item at once — essential for smooth scrolling with thousands of rows.

The jaadu helps one rider out of a giant wheel cabin as a new rider waits to step in.

Think of it like this

The giant wheel at the mela

A giant wheel has only a few cabins, but hundreds of people ride it through the evening. Cabins come round, people get off, new ones get on. A virtual list reuses a few rows the same way while you scroll through thousands of items.

Watch it happen

A 400px window over 10,000 rows

row 0
0
row 1
1
row 2
2
…
3
row 9
4
row 10
5
row 11
6
…
7
row 9999
8

1/410,000 rows of 40px each. getTotalSize makes the spacer 400,000px tall so the scrollbar looks right.

The big ideas

What you need to know

Render only what's visible

Keep a small window of rows in the DOM, plus a few extra above and below for smooth scrolling.

Fake the full height

An inner spacer with the full list's height keeps the scrollbar correct even though most rows don't exist.

Use a library

TanStack Virtual and react-window handle the maths. Writing it yourself is a good exercise, not a good production choice.

When to bother

Lists under a few hundred simple rows are usually fine. Reach for virtualization when scrolling starts to stutter.

See it in code

Walk through the code

tsx
1import { useRef } from "react";2import { useVirtualizer } from "@tanstack/react-virtual";3export function BigList({ rows }: { rows: string[] }) {4  const parentRef = useRef<HTMLDivElement>(null);5  const v = useVirtualizer({ count: rows.length, getScrollElement: () => parentRef.current, estimateSize: () => 40 });6  return (7    <div ref={parentRef} style={{ height: 400, overflow: "auto" }}>8      <div style={{ height: v.getTotalSize(), position: "relative" }}>9        {v.getVirtualItems().map((item) => (10          <div key={item.key} style={{ position: "absolute", top: 0, width: "100%", height: item.size,11            transform: `translateY(${item.start}px)` }}>{rows[item.index]}</div>12        ))}13      </div>14    </div>15  );16}

1/3useVirtualizer needs the row count, the scroll container and a row height estimate.

With 10,000 rows, the DOM holds only about a dozen at a time. Inspect it in DevTools while scrolling and watch the same few divs change.

Save yourself the pain

Common mistakes

Oops

Virtualizing a list of 30 items.

Do this instead

It adds complexity for no gain. Use it when you have hundreds or thousands of rows and can see lag.

Oops

Forgetting to give the scroll container a fixed height.

Do this instead

The virtualizer needs a container with a set height and overflow: auto to know what's visible.

Your mission

10,000 contacts, zero lag

~35 min

Quick check

In a virtualized list of 10,000 items, roughly how many rows are in the DOM at once?

Go deeper

  1. 1Render a 10,000-item list normally and observe the scroll jank.
  2. 2Rebuild it using a virtualization library (e.g. TanStack Virtual) and compare.
  3. 3Learn the core idea: only mount DOM nodes for what's actually visible.

More topics at this stop

Stuck on this topic? Ask an Insider

Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.

Download
InsideEdge