System Design

Virtualization

mediumperformance

Virtualization

Asked In

MetaGoogleAirbnbDatadogSalesforceNotion

Key Challenges

  • ·Rendering 10k+ rows without freezing the main thread
  • ·Measuring variable row heights without scroll jumps
  • ·Keeping focus and find-in-page usable with a windowed DOM
  • ·Knowing when virtualization is the wrong tool

Guided Answer

Read this path first — then deepen in RADIO below

Simple Explanation

Imagine a notifications list with 50,000 rows.

The first paint looks fine. Then the user scrolls — the tab freezes, the fan spins, and the page feels broken.

Virtualization fixes this. You only put the rows on screen into the DOM. The rest of the list is empty scroll space, not real elements.

Mental Model

Think of a theater that only lights the seats people can see.

You keep a tall empty spacer for the full list height. Inside it, you mount ~20–40 visible rows (plus a little overscan above and below). When the user scrolls, you slide that window. Fixed row height = simple math. Variable height = estimate, measure, cache, then re-anchor so the list does not jump.

Diagrams

Architecture — what exists in the DOM
Component node + responsibility notes
ScrollContainer(overflow: scroll)
HeightSpacer(height = full list)
... not mounted(above)
VisibleRows(viewport + overscan)
Row 40
Row 41only these are real DOM
Row 42
...
... not mounted(below)
Logic: scrollTopstartIndex / endIndex → mount that slice
Flow — naive vs fix
Component node + responsibility notes
Naive
1

data.map — (Row)

2

50,000 DOM nodes

3

lag on every scroll

4

read scrollTop + viewport

5

compute startIndex / endIndex + overscan

6

mount only that slice inside the spacer

7

variable height: estimate — measure → cache → re-anchor

Step-by-Step

1

Scene — the laggy list

Product dumps tens of thousands of rows into a table or feed. Scrolling hitchs. That is your opening story — not a library name.

Say this

I would start from DOM cost: layout scales with mounted nodes, not with how many rows exist in data.

2

Naive move

Render everything with data.map(Row). Fine for 30 rows in Storybook. Dies at 10k+ because every scroll pays for the whole list.

  • ·Long freezes while scrolling
  • ·Memory grows with list length
  • ·Updates and focus get expensive

Say this

The naive .map() breaks when cost follows list length instead of screen size.

3

The fix — window + spacer + overscan

Track scroll position and viewport height. Compute which indexes are visible. Render only that slice inside a tall spacer that represents the full list. Overscan adds a few extra rows so fast flings do not flash empty space.

totalHeight = count * rowHeight
start = floor(scrollTop / rowHeight) - overscan
end   = ceil((scrollTop + viewport) / rowHeight) + overscan

Say this

I keep a spacer for total height and only mount the visible index range plus overscan.

4

Variable heights

Chat bubbles are not one height. Estimate first, measure after paint, cache by index, then fix the spacer. If a row above the viewport changes height, re-anchor scroll so the row under the user's eyes does not jump.

Say this

For variable rows I estimate, measure, cache, and re-anchor — otherwise the list jumps.

5

Trade-offs to say out loud

Browser find (Cmd/Ctrl+F) only sees mounted rows. scrollToId needs your height model. Focus cannot assume every row exists. Call these out before the interviewer asks.

  • ·Find-in-page is incomplete
  • ·scrollToId needs cached offsets
  • ·Sticky headers need extra care

Say this

I would mention find-in-page, focus, and scroll-to-id as trade-offs up front.

6

When NOT to virtualize

Skip it for a 15-item dropdown, a short settings page, or print layouts. Virtualization has cost — measurement bugs and harder tests. Use it when DOM budget is the real problem.

Say this

I would not virtualize a small list — the complexity is not free.

Final Answer

I keep DOM cost tied to the viewport. A VirtualList tracks scrollTop, computes a visible range with overscan, and places those rows inside a spacer for total height. Fixed heights use simple math. Variable heights use estimate → measure → cache → re-anchor. I call out find-in-page and focus trade-offs, and I skip virtualization for small lists.

Deep Dive — RADIO Framework

2-Minute Answer — High-Level TL;DR

Read this before your interview

HLD Interview Focus

I would not mount ten thousand DOM nodes for a feed or table. Virtualization keeps only the visible window plus a small overscan band in the DOM. I track scrollTop, compute start/end indexes from row heights, and absolute-position those rows inside a tall spacer that represents total height. For variable heights I estimate, measure after paint, cache by index, and re-anchor so a remeasure does not jump the list. I would not virtualize a 20-item dropdown.

My Approach: Building the Solution

How to think about this problem

Start from the failure: a product manager dumps 50k notifications into a list and the tab locks for half a second on every scroll. Then show the windowing fix before naming a library.

Why This Approach?

Junior answers call .map() and hope. That dies when layout/paint scale with list length. Staff answers window the list, explain overscan, and volunteer trade-offs (Cmd-F, focus, scroll-to-id) before the interviewer fishes for them.

A theater only lights the seats people can see. Virtualization lights the viewport — the rest of the audience exists as empty space in the scrollable stage, not as real chairs.

R

Requirements: What Makes a Great Solution?

Clarify scope before designing anything

Requirements Exploration Questions

Ask your interviewer these questions to refine requirements

How large is the list, really?

  • ·Hundreds, thousands, or hundreds of thousands?
  • ·Fixed vs variable row height?
  • ·Horizontal virtualization needed?

What interactions must survive windowing?

  • ·Scroll to item by id
  • ·Keyboard focus
  • ·Sticky headers
  • ·Find-in-page

Functional Requirements

Must Have — MVP

  • Viewport window + overscan
  • Spacer height = sum of row heights
  • Scroll position → visible index range
  • Stable keys for mounted rows

Advanced (If Time Permits)

  • +Dynamic measurement cache
  • +Two-axis virtualization (spreadsheet)
  • +Sticky columns/headers with virtualized body

Non-Functional Requirements

Interview bar

  • ·Name the invariant in one sentence
  • ·Give one concrete failure of the naive approach
  • ·State when the pattern is the wrong tool
A

Architecture (Conceptual)

Component structure, data flow, rendering strategy

Virtualized Data Table Engine

10,000 Records • Real-time Sort & Filter • Fixed DOM Slice
14 Mounted NodesMemory Saved: 99.8%
Showing 10,000 / 10,000 matches
#1
Ava Chen
ava.chen0@tech.io
Engineer
Active
$1,200
#2
Liam Singh
liam.singh1@tech.io
Designer
Pending
$1,337
#3
Noah Aziz
noah.aziz2@tech.io
Product Manager
Inactive
$1,474
#4
Emma Wilson
emma.wilson3@tech.io
Data Scientist
Active
$1,611
#5
Olivia Garcia
olivia.garcia4@tech.io
Architect
Pending
$1,748
#6
Ethan Rodriguez
ethan.rodriguez5@tech.io
Engineer
Inactive
$1,885
#7
Mia Rossi
mia.rossi6@tech.io
Designer
Active
$2,022
#8
Lucas Mori
lucas.mori7@tech.io
Product Manager
Pending
$2,159
#9
Aria Nguyen
aria.nguyen8@tech.io
Data Scientist
Inactive
$2,296
#10
James Lee
james.lee9@tech.io
Architect
Active
$2,433
#11
Sofia Clark
sofia.clark10@tech.io
Engineer
Pending
$2,570
#12
Benjamin Silva
benjamin.silva11@tech.io
Designer
Inactive
$2,707
#13
Charlotte Costa
charlotte.costa12@tech.io
Product Manager
Active
$2,844
#14
Daniel Kim
daniel.kim13@tech.io
Data Scientist
Pending
$2,981
Virtual window height: 480,000px • Container height: 400pxSmooth 60 FPS scrolling guaranteed via CSS translateY

Component Hierarchy

Top-Level: VirtualList

  • ·Compute visible range from scrollTop + viewport
  • ·Maintain height estimates and measured cache
  • ·Position rows; expose scrollToIndex
component-hierarchy-tree/
ONE APP · CLEAR BOUNDARIES
VirtualList
ScrollContainer
HeightSpacer
VisibleRows[](absolute positioned)
imports flow one way: features use ui, data-access, and utils; utils imports nothing

React-Specific Architecture Patterns

VirtualList

  • ·Compute visible range from scrollTop + viewport
  • ·Maintain height estimates and measured cache
  • ·Position rows; expose scrollToIndex
architecture-diagram/
ONE APP · CLEAR BOUNDARIES
ScrollContainer(overflow: scroll)
HeightSpacer(height = full list)
... not mounted ...
VisibleRows (viewport + overscan) ← only real DOM
Row 40
Row 41
Row 42
... not mounted ...
scrollTop → startIndex / endIndex → mount that slice
imports flow one way: features use ui, data-access, and utils; utils imports nothing

Why React Query + Zustand?

Keep teaching state next to the concept; libraries are secondary.

D

Data Model (API + Entities + Cache)

Entities, interfaces, cache shape, consistency rules

Minimal entities that make the concept concrete in code reviews and whiteboards.

HeightModel

Source: Client
Belongs to: VirtualList
Fields: estimates[], measuredByIndex, totalSize
concept.ts
function visibleRange(scrollTop: number, viewport: number, rowHeight: number, count: number, overscan: number) {
  const start = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
  const end = Math.min(count, Math.ceil((scrollTop + viewport) / rowHeight) + overscan);
  return { start, end, offsetY: start * rowHeight, totalHeight: count * rowHeight };
}
I

Interface Design (React Integration)

API contracts, hooks, integration patterns

Contracts are light for core concepts — prove the idea, not a full product API.

O

Optimization (Performance + Scale)

Rendering, media, network, and memory optimizations

I would not mount ten thousand DOM nodes for a feed or table. Virtualization keeps only the visible window plus a small overscan band in the DOM. I track scrollTop, compute start/end indexes from row heights, and absolute-position those rows inside a tall spacer that represents total height. For variable heights I estimate, measure after paint, cache by index, and re-anchor so a remeasure does not jump the list. I would not virtualize a 20-item dropdown.

Sticky rules

  • ·DOM cost scales with mounted nodes — virtualize so only the viewport (+ overscan) exists.
  • ·Estimate → measure → cache height → re-anchor scroll, or variable rows will jump.
  • ·Overscan buys smoothness; too much overscan recreates the original problem.
  • ·Do not virtualize small lists — complexity is not free.

Optimistic Updates

Junior answers call .map() and hope. That dies when layout/paint scale with list length. Staff answers window the list, explain overscan, and volunteer trade-offs (Cmd-F, focus, scroll-to-id) before the interviewer fishes for them.

optimisticUpdate.ts
function visibleRange(scrollTop: number, viewport: number, rowHeight: number, count: number, overscan: number) {
  const start = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
  const end = Math.min(count, Math.ceil((scrollTop + viewport) / rowHeight) + overscan);
  return { start, end, offsetY: start * rowHeight, totalHeight: count * rowHeight };
}

Remember

DOM cost scales with mounted nodes — virtualize so only the viewport (+ overscan) exists.

Say this in the interview

I would not mount ten thousand DOM nodes for a feed or table.

+

Bonus: Implementation Cookbook (Optional)

LLD snippets — only when asked by interviewer

Follow-ups interviewers use after you nail the core idea.

Included (Minimal)

  • ·Dynamic measurement cache
  • ·Two-axis virtualization (spreadsheet)
  • ·Sticky columns/headers with virtualized body

Key Takeaways

  • DOM cost scales with mounted nodes — virtualize so only the viewport (+ overscan) exists.
  • Estimate → measure → cache height → re-anchor scroll, or variable rows will jump.
  • Overscan buys smoothness; too much overscan recreates the original problem.
  • Do not virtualize small lists — complexity is not free.