Lesson 49 of 80
Rendering 10,000 rows when only 20 fit on screen wastes almost all of that work — virtualization renders just the visible slice, and fakes the rest with a spacer.
16 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
A list of 10,000 items only ever shows maybe 20 rows in the viewport at once. Rendering all 10,000 DOM nodes anyway is mostly wasted work — most of them are scrolled off-screen and invisible. List virtualization renders only the rows currently in view (plus a small buffer), and uses a full-height spacer element so the scrollbar still behaves as if all 10,000 rows were really there. As the user scrolls, the component recalculates which slice is visible and swaps the rendered rows out.
function VirtualList({ items, rowHeight = 40, height = 400 }) {
const [scrollTop, setScrollTop] = useState(0);
const startIndex = Math.floor(scrollTop / rowHeight);
const visibleCount = Math.ceil(height / rowHeight) + 2; // 2 rows of overscan
const endIndex = Math.min(items.length, startIndex + visibleCount);
const visibleItems = items.slice(startIndex, endIndex);
return (
<div
style={{ height, overflowY: "auto" }}
onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}
>
<div style={{ height: items.length * rowHeight, position: "relative" }}>
{visibleItems.map((item, i) => {
const index = startIndex + i;
return (
<div
key={item.id}
style={{ position: "absolute", top: index * rowHeight, height: rowHeight }}
>
{item.label}
</div>
);
})}
</div>
</div>
);
}
The outer <div> is a fixed-height scroll container — it's the only thing the browser actually has to lay out at real size. The inner <div> is a spacer, sized to items.length * rowHeight, so the scrollbar's length and position are correct even though only a handful of rows are actually in the DOM. Each visible row is absolutely positioned at its true offset (index * rowHeight), so removing rows from the middle of the visible range doesn't shift the rows around it. visibleCount adds two rows of "overscan" beyond what's strictly visible, so fast scrolling doesn't flash an empty gap before the next scroll event's render catches up.
Note the key is item.id, not the row's position in the currently rendered slice — startIndex shifts on every scroll, so keying by the slice position would make React think every row is a brand-new one each time you scroll, discarding and remounting instead of reusing.
items array still has to be in memory and in state — virtualization doesn't reduce how much you fetched, only how much of it gets rendered as actual elements at once.This connects to the earlier lesson on keys and lists: the key still has to identify the underlying item itself, not the row's position in whatever's currently rendered — and virtualization is exactly the case where those two numbers constantly diverge, since the rendered slice's start index changes on every scroll.