Render 100,000 rows into the DOM and the browser chokes — layout, paint, and memory all blow up. Virtual scrolling (windowing) fixes it: only the ~20 rows actually on screen exist as DOM nodes. The trick is faking the rest — give the scroll container the full height so the scrollbar is honest, then translate the handful of real rows to the right place as the user scrolls. This is the engine inside react-window, TanStack Virtual, and every performant infinite feed.
Implement virtualScrollList(options) — the pure math that, given the scroll position and row metrics, returns which rows to render and where. No DOM needed.
function virtualScrollList({ itemCount, itemHeight, viewportHeight, scrollTop, overscan }) {
return { startIndex, endIndex, items, totalHeight, offsetTop, visibleCount };
}
// 1000 rows of 50px, a 500px viewport, scrolled 500px down
virtualScrollList({ itemCount: 1000, itemHeight: 50, viewportHeight: 500, scrollTop: 500 });
// { startIndex: 10, endIndex: 19, offsetTop: 500, totalHeight: 50000, visibleCount: 10, items: [...] }
// overscan renders a few extra rows above/below to hide fast-scroll gaps
virtualScrollList({ itemCount: 1000, itemHeight: 50, viewportHeight: 500, scrollTop: 500, overscan: 3 });
// startIndex 7, endIndex 22
startIndex = floor(scrollTop / itemHeight), count = ceil(viewportHeight / itemHeight) (plus a partial row). Never loop over all itemCount rows.totalHeight sizes the container — itemCount * itemHeight, so the scrollbar reflects the full list; offsetTop (startIndex * itemHeight) positions the rendered window.overscan extra rows on each side (clamped to [0, itemCount-1]) so a fast scroll doesn't flash blank.scrollTop to [0, totalHeight - viewportHeight]; an empty list renders nothing (endIndex -1).Unlock the solution & editor
Runnable editor + tests
Solve in the browser with instant Jest feedback.
Detailed solutions
Walkthroughs, edge cases, and complexity notes.
Multi-framework variants
React, Vue, Vanilla, Angular — same question, different stacks.