ESC

Type to search the knowledge base.

Virtual Lists Performance

Render only visible rows: windowing mental model, overscan, scroll issues, and accessibility tradeoffs.

advanced3 min read
  • performance
  • virtualization
  • lists
  • react
  • inp

Rendering 10,000 DOM nodes for a list is a style/layout/paint tax that destroys scroll and INP. Virtualization (windowing) renders only visible rows plus a small overscan, recycling nodes as the user scrolls.

Docs: web.dev virtualize (concept), MDN content-visibility.

Mental model

scrollTop + viewportHeight → visible index range
render rows [start - overscan, end + overscan]
position with translate/absolute offsets
total height = rowCount * rowHeight (or estimated)

Libraries: react-window, react-virtuoso, TanStack Virtual, etc. Understand the model before copying props.

Fixed vs variable height

Fixed height is simplest and cheapest. Variable height needs measurement and can thrash if misimplemented — cache heights, avoid sync layout in loops — layout thrashing.

// sketch
function visibleRange(scrollTop, viewport, rowHeight, n, overscan) {
  const start = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
  const end = Math.min(n, Math.ceil((scrollTop + viewport) / rowHeight) + overscan);
  return [start, end];
}

CSS-only help

.row {
  content-visibility: auto;
  contain-intrinsic-size: 0 48px;
}

content-visibility: auto skips rendering offscreen content in supporting browsers — good complement, not always a full virtualizer replacement for huge lists with complex rows.

Footguns

  1. Keyboard / a11y — ensure focusable items and aria expectations still work; announce counts.
  2. Find-in-page — may not search unmounted rows.
  3. SEO — critical content shouldn’t exist only in a windowed client list.
  4. Scroll restoration — jump to index APIs needed.
  5. Sticky headers / multi-column — harder; pick libraries with support.
  6. Infinite load — append data without remounting the whole window.

When not to virtualize

Short lists (< ~100 simple rows) — complexity not worth it. Profile first.

Interview out-loud

“Virtual lists render only visible rows plus overscan so DOM stays small. I fix row height strategy, preserve a11y focus, and reach for content-visibility for lighter cases. Huge unvirtualized tables show up as layout and INP cost.”

How this shows up in interviews

Be ready to define the metric or technique in one sentence, name one measurement approach (DevTools panel, web-vitals, or headers), and cite a concrete fix you would try first. Walk through a before/after: what the waterfall or flame chart showed, what you changed, and which percentile moved. Mention a tradeoff (complexity, caching correctness, or third-party business constraints) so the answer doesn’t sound like a blog checklist.

Production guardrails

Ship behind a flag when the change is risky, watch field p75 for the affected template for at least a few days, and keep a rollback path. Pair lab verification (throttled Performance/Network) with RUM so you don’t celebrate a Lighthouse-only win. Document the owner of any ongoing budget or third-party exception.

Further depth

Teams often under-invest in this topic until an incident or CWV regression. Schedule a one-hour drill: reproduce the failure mode in DevTools, list the top three mitigations for your stack, and file tickets with owners. Revisit after the next major feature that touches networking, rendering, auth, or third parties — those are the moments regressions land. Keep primary documentation links in the runbook so on-call is not searching chat history at 2am.

Concrete artifacts to leave behind: a short architecture note, a CI assertion or header snapshot, and a dashboard panel (lab or field) that would have caught the last bug. Teaching the rest of the team the mental model matters as much as the one-line fix.

Further reading

Related guides