Implement a virtualized list for 100,000 rows
Render only the slice of rows within the scroll viewport plus overscan. Maintain total scroll height via a spacer or absolutely-positioned container. For 100k rows, fixed heights are simplest; dynamic heights require measurement caches. Use `@tanstack/react-virtual` in production; for an interview, code it from scratch using a scroll listener + visible-range math.
Rendering 100,000 DOM nodes is not viable — that's a 16ms frame burned just on layout. The fix is windowing: render ~30 visible rows, fake the rest with spacers so the scrollbar still represents 100k.
Core math.
totalHeight = count × rowHeight
firstVisible = Math.floor(scrollTop / rowHeight)
lastVisible = Math.ceil((scrollTop + viewportHeight) / rowHeight)Render [firstVisible − overscan, lastVisible + overscan]. Position each rendered row at top = index × rowHeight (absolute), or pad with a spacer of height firstVisible × rowHeight.
Minimal from-scratch implementation (fixed height).
function VirtualList({ items, rowHeight = 40, height = 600, overscan = 5, renderRow }) {
const [scrollTop, setScrollTop] = useState(0);
const total = items.length * rowHeight;
const first = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
const last = Math.min(
items.length,
Math.ceil((scrollTop + height) / rowHeight) + overscan
);
const visible = [];
for (let i = first; i < last; i++) {
visible.push(
<div
key={items[i].id}
style={{
position: "absolute",
top: i * rowHeight,
left: 0,
right: 0,
height: rowHeight,
}}
>
{renderRow(items[i])}
</div>
);
}
return (
<div
onScroll={e => setScrollTop((e.target as HTMLDivElement).scrollTop)}
style={{ height, overflow: "auto", position: "relative" }}
>
<div style={{ height: total, position: "relative" }}>{visible}</div>
</div>
);
}That's the whole story for fixed-height. ~30 nodes in the DOM no matter how big items gets.
Then the questions get harder.
1. Dynamic row heights. Don't know the height until rendered. Two strategies:
- Estimate + measure + correct. Use an estimate (
estimateSize: () => 40). After mount,ResizeObservermeasures real heights and caches them. On next render, position rows by summing cached heights. Re-render when the cache invalidates near visible range. TanStack Virtual does this. - Estimate-only with no correction. Cheap; scrollbar position is approximate. Fine for chat-like UIs where exact scroll position doesn't matter.
The senior detail: when a row's measurement comes in after it scrolled into view, you may need to anchor the scroll position so the user doesn't see content jump. TanStack handles this via "scroll padding adjustment."
2. Scroll position restoration. On route change → back, restore scroll to the same row. Store the id of the first-visible row, not pixel offset (heights may differ across mounts).
3. Keyboard navigation. Up/Down arrows need to scroll the focused row into view even if it's not currently rendered. Implement a scrollToIndex(i) that sets scrollTop = i * rowHeight and waits a frame before focusing.
4. Ctrl+F doesn't work. Browser find doesn't see virtualized rows. Provide an in-app search, or use content-visibility: auto if you can render the whole DOM but skip rendering off-screen children.
5. Selection across thousands of rows. Store a Set of selected ids, not booleans on the row. Shift-click range select can compute by indices.
6. Variable column count (grid virtualization). Same math in two dimensions. @tanstack/react-virtual exposes a separate useVirtualizer per axis.
7. Sticky rows / group headers. A row that's at the top of its group should stick to the top of the viewport. Implement via a separate "pinned" container outside the virtualizer's main slot.
8. RTL. Scroll math inverts; scrollLeft is negative in WebKit historically. Use the lib unless you really want to deal with this.
Production recommendation. Don't ship a hand-rolled virtualizer beyond a prototype. Use @tanstack/react-virtual (or react-virtuoso for chat/grouped lists). They handle dynamic heights, scroll anchoring, RTL, smooth scroll-to-index, and have been tested on millions of edge cases.
Why interviewers ask. The question reveals whether the candidate understands the browser rendering pipeline (10k+ DOM is bad), React's reconciliation (keys matter, memo on rows), and practical UX (scrollbar must represent full size, keyboard nav, focus restoration). The from-scratch fixed-height version above is enough; the bonus points come from naming the dynamic-height edge cases.
Follow-up questions
- •How does TanStack Virtual handle dynamic row heights?
- •What's the trade-off between virtualization and `content-visibility: auto`?
- •How would you implement scroll-position restoration on route navigation?
- •Why is index-as-key catastrophic for a virtualized list?
Common mistakes
- •Using array index as React `key` — rows shuffle on scroll.
- •Forgetting to position absolutely or pad with a spacer — scroll content has no height.
- •Recomputing visible range without overscan — flashes blank rows on fast scroll.
- •Listening to `onScroll` without throttling / rAF — re-renders 60+ times per second.
Performance considerations
- •Throttle scroll handler with rAF.
- •Memoize the row component; otherwise every scroll re-renders every visible row.
- •Apply `contain: layout paint` on the row to isolate work.
- •Avoid expensive children inside rows; render placeholders until idle.
Edge cases
- •Window resize must recompute visible range (height changes).
- •Very small datasets — bail out and render directly.
- •Browser zoom — heights change; re-measure on `visualviewport` resize.
Real-world examples
- •Linear, Notion, Slack message history, VSCode file tree — all use virtualization.