Virtualization

The problem

The naive approach to rendering a 50,000-row data grid is to create 50,000 DOM nodes. The browser then spends its time laying out, painting, and reflowing elements the user can’t even see. Virtualization (also called windowing) fixes this by rendering only the rows currently visible in the viewport, plus a small buffer above and below for smooth scrolling.

Libraries like react-window and TanStack Virtual provide the infrastructure. For complex enterprise grids with frozen columns, dynamic row heights, and horizontal scrolling across hundreds of columns, teams often build a custom implementation tuned to their data shape, or adopt AG Grid. We use AG Grid at Coinbase for internal, data-heavy apps; it's excellent and highly customizable.

Step through it

Scroll the 10,000-row inbox and watch the DOM node count stay tiny. Step through the concepts, toggle naive versus virtualized, and adjust the overscan to see the math in action.

SYSTEM DESIGN · FRONTENDList Virtualization
16 DOM nodes
Overview
Inbox· 10,000 messagesVIEWPORT
Showing rows 18scroll / drag ↕
AC
Ava Chen1:00 AM
Re: Q3 roadmap review
#1
LS
Liam Silva2:17 AM
New comment on your doc
#2
NR
Noah Rossi3:34 AM
Payment received
#3
ES
Emma Singh4:51 AM
Can you review this PR?
#4
OC
Olivia Costa5:08 AM
Deploy succeeded ✓
#5
EM
Ethan Mori6:25 AM
Welcome to the team!
#6
MA
Mia Aziz7:42 AM
Design feedback needed
#7
LK
Lucas Kim8:59 AM
Your order has shipped
#8
AN
Aria Nguyen9:16 AM
Reminder: 1:1 at 3pm
#9
LL
Leo Lopez10:33 AM
Re: bug in checkout
#10
ZA
Zoe Adler11:50 AM
Lunch on Friday?
#11
KF
Kai Falk12:07 AM
Weekly digest
#12
WHAT THE USER SEES
the actual DOM treeVIRTUALIZED
16
mounted / 10,000 rows
60 fps · smooth
<Row>#1VISIBLE
<Row>#2VISIBLE
<Row>#3VISIBLE
<Row>#4VISIBLE
<Row>#5VISIBLE
<Row>#6VISIBLE
<Row>#7VISIBLE
<Row>#8VISIBLE
<Row>#9OVERSCAN
<Row>#10OVERSCAN
<Row>#11OVERSCAN
<Row>#12OVERSCAN
<Row>#13OVERSCAN
<Row>#14OVERSCAN
<Row>#15OVERSCAN
<Row>#16OVERSCAN
Each box is one mounted <Row>. Bright = on screen, dim = overscan buffer. Scroll and watch them recycle — the count never grows.
0 · Render only what fits on screenA list of 10,000 messages doesn't need 10,000 DOM nodes. Virtualization — or windowing — mounts only the handful of rows inside the scroll viewport and recycles them as you scroll. Drag the scrollbar: the indices race into the thousands, but the DOM stays tiny.
useVirtualizer.js
const ROW_H = 44
const total = 10000
function onScroll(scrollTop) {
const first = Math.floor(scrollTop / ROW_H)
const count = Math.ceil(viewH / ROW_H)
const start = first - overscan
const end = first + count + overscan
const slice = items.slice(start, end)
padTop = start * ROW_H
padBottom = (total - end) * ROW_H
render(padTop, slice, padBottom)
}
overscan 2

How windowing works

No special API: it is arithmetic on the scroll position. Give rows a known height and read scrollTop on every scroll.

const first = Math.floor(scrollTop / ROW_H);
const count = Math.ceil(viewportHeight / ROW_H);
const start = first - overscan;
const end = first + count + overscan;
const slice = items.slice(start, end); // the only mounted rows

Render slice between two spacer divs so row positions and the scrollbar stay correct:

const padTop = start * ROW_H;
const padBottom = (total - end) * ROW_H;

padTop + slice + padBottom always equals total * ROW_H (440,000px here), so the container is the full height it would be with every row present. Throttle the scroll handler with requestAnimationFrame. Overscan is the few extra rows each side (2 to 5) that keep a fast flick from flashing blank.

Fixed versus variable height

Fixed height is trivial: every offset is index * ROW_H. Variable height must be measured: render with an estimate, measure the real height after layout, cache it by index, and anchor the scroll position so a re-measured row does not make the list jump.

Trade-offs

You take back behaviors the browser handled for free:

  • Ctrl or Cmd + F only finds mounted rows.
  • Linking or scrolling to an off-screen row needs manual offset math.
  • Keep list semantics intact and do not strand focus on an unmounted row.
  • Drag and drop reorders by data, since the target may be unmounted.

Reach for a library (react-window, @tanstack/react-virtual, react-virtuoso); hand-roll only to learn or customize.

Why interviewers ask this

"The list could have thousands of items" is a standard scaling turn, and virtualization is the expected answer. It shows you know rendering cost scales with DOM size, not screen size. It recurs in feed, chat, table, and board questions.