Why Virtualized List Is Asked in Interviews
A virtualized list is a strong frontend machine coding question because it tests much more than UI rendering. It checks whether a candidate understands DOM performance, scroll handling, rendering cost, layout calculation, and optimization techniques for large datasets. Unlike simpler questions, this one directly reveals whether the developer knows how browsers behave when too many elements are rendered at once.
In real applications, rendering thousands of DOM nodes can slow down scrolling, increase memory usage, and hurt user experience. A virtualized list solves this by rendering only the items visible on screen, plus a small buffer around them.
What Problem Are We Solving?
Suppose an application needs to show 10,000 records such as users, products, logs, or messages. A naive implementation would map over all 10,000 items and render them together. Even if each item is simple, the browser still has to create, style, layout, and paint all those nodes. That is expensive.
A virtualized list takes a different approach. It keeps the scroll behavior of a full list, but only renders the small subset of rows that are currently visible inside the viewport. As the user scrolls, old rows are removed and new rows are inserted.
Visualizing the User Experience
Imagine a log viewer with 50,000 entries. The user scrolls as if the entire list exists in the DOM, but under the hood only perhaps 20 to 40 rows are actually mounted at any moment. The container has the height of the full list, so the scrollbar feels natural, while the rendered rows move within that space based on scroll position.
The Core Idea Behind Virtualization
The key idea is simple: the browser does not need every item in the DOM at the same time. It only needs the rows near the viewport. If each row has a fixed height, then the component can calculate which items should be visible from the current scroll position.
For example, if each row is 50 pixels tall and the scroll position is 500 pixels, then the first visible row is roughly item index 10. If the container height is 300 pixels, then around 6 rows are visible. With a small overscan buffer, the component may render rows 8 through 18 instead of the full dataset.
What the Interviewer Usually Expects
- Render a large list efficiently
- Show only visible items based on scroll position
- Maintain a natural scrollbar for the full dataset
- Update visible rows as the user scrolls
- Avoid rendering all items at once
- Keep calculations clean and predictable
Some interviewers may also ask about variable row heights, overscan buffers, sticky headers, or integration with APIs. But the core question is usually about fixed-height row virtualization.
How to Think About the Problem Before Coding
Before writing JSX, the problem should be broken into parts. First, decide the height of each row. Then calculate the total height of the complete list. After that, determine which items are visible based on scrollTop and container height. Finally, render only those items in an absolutely positioned inner layer.
This is an interview problem where mathematical clarity matters. If the calculations are correct, the rendering logic becomes much easier.
Step 1: Fixed Inputs for the Problem
A basic virtualized list generally starts with three known values: the full dataset, the row height, and the container height.
const ITEM_HEIGHT = 50;
const CONTAINER_HEIGHT = 300;
const items = Array.from({ length: 10000 }, (_, i) => `Item ${i + 1}`);Here, every row has the same height. This assumption simplifies calculations and is commonly used in interviews.
Step 2: Understanding Total Height
Even though only a few rows will be rendered, the scrollbar must behave as if the full list exists. That means the component needs a wrapper whose height equals the total list height.
const totalHeight = items.length * ITEM_HEIGHT;If there are 10,000 items and each row is 50 pixels high, then the full scrollable height is 500,000 pixels. The browser uses that height to produce the scrollbar.
Step 3: Tracking Scroll Position
The rendered window depends on scrollTop. When the user scrolls, the component calculates which row index is currently near the top of the viewport.
const [scrollTop, setScrollTop] = useState(0);
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
setScrollTop(e.currentTarget.scrollTop);
};This scroll value drives the visible range calculation. It is the most important state in the component.
Step 4: Calculating Visible Indices
Once scrollTop is known, the first visible index is scrollTop divided by item height. The number of visible rows is container height divided by item height. A small overscan buffer is usually added so rows just above and below the viewport are also rendered. This makes scrolling smoother.
const OVERSCAN = 2;
const startIndex = Math.max(
0,
Math.floor(scrollTop / ITEM_HEIGHT) - OVERSCAN
);
const visibleCount =
Math.ceil(CONTAINER_HEIGHT / ITEM_HEIGHT) + OVERSCAN * 2;
const endIndex = Math.min(items.length, startIndex + visibleCount);For example, if scrollTop is 500, item height is 50, and container height is 300, then the first visible row is around index 10. With overscan, rendering may begin from index 8 and continue until about index 18.
Step 5: Slicing Only the Visible Items
Now that the visible range is known, the component slices only that section from the array.
const visibleItems = items.slice(startIndex, endIndex);This is where the optimization happens. Even though the dataset contains thousands of items, only a handful are actually rendered.
Step 6: Positioning Visible Rows Correctly
If only rows 8 through 18 are rendered, they still need to appear at the correct vertical position inside the full list. This is usually done by absolutely positioning each visible row or translating an inner wrapper.
const offsetY = startIndex * ITEM_HEIGHT;If startIndex is 8 and each row is 50 pixels high, then the first rendered row should appear 400 pixels from the top of the full list space.
Complete Example Component
import React, { useMemo, useState } from "react";
const ITEM_HEIGHT = 50;
const CONTAINER_HEIGHT = 300;
const OVERSCAN = 2;
const items = Array.from({ length: 10000 }, (_, i) => `Item ${i + 1}`);
export default function VirtualizedList() {
const [scrollTop, setScrollTop] = useState(0);
const totalHeight = items.length * ITEM_HEIGHT;
const { startIndex, endIndex, visibleItems, offsetY } = useMemo(() => {
const startIndex = Math.max(
0,
Math.floor(scrollTop / ITEM_HEIGHT) - OVERSCAN
);
const visibleCount =
Math.ceil(CONTAINER_HEIGHT / ITEM_HEIGHT) + OVERSCAN * 2;
const endIndex = Math.min(items.length, startIndex + visibleCount);
const visibleItems = items.slice(startIndex, endIndex);
const offsetY = startIndex * ITEM_HEIGHT;
return { startIndex, endIndex, visibleItems, offsetY };
}, [scrollTop]);
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
setScrollTop(e.currentTarget.scrollTop);
};
return (
<div
onScroll={handleScroll}
style={{
height: CONTAINER_HEIGHT,
overflowY: "auto",
border: "1px solid #ccc",
position: "relative",
}}
>
<div
style={{
height: totalHeight,
position: "relative",
}}
>
<div
style={{
position: "absolute",
top: offsetY,
left: 0,
right: 0,
}}
>
{visibleItems.map((item, index) => (
<div
key={startIndex + index}
style={{
height: ITEM_HEIGHT,
display: "flex",
alignItems: "center",
padding: "0 12px",
borderBottom: "1px solid #eee",
boxSizing: "border-box",
background: "#fff",
}}
>
{item}
</div>
))}
</div>
</div>
</div>
);
}How the Flow Works Internally
Initially, scrollTop is 0, so the component renders the first few rows. When the user scrolls, scrollTop changes. The component recalculates startIndex and endIndex, slices the new visible range, and moves the rendered block downward using offsetY. The user experiences smooth continuous scrolling, even though most rows never exist in the DOM at the same time.
Why Overscan Matters
Overscan means rendering a few extra rows above and below the visible viewport. Without it, fast scrolling may briefly reveal blank gaps before the new rows mount. With overscan, the experience feels smoother because nearby rows are already rendered just before they enter view.
For example, if exactly 6 rows fit in the viewport, rendering 10 or 12 rows with a small buffer often gives better perceived performance without significantly increasing DOM size.
Common Mistakes Candidates Make
- Rendering the full list and calling it virtualized
- Forgetting to preserve full scroll height
- Using wrong index calculations, causing skipped or repeated rows
- Not adding overscan, which leads to flickering
- Using unstable keys that break row reuse
- Tightly coupling calculations and rendering in a messy way
A strong solution keeps the math simple, the rendering clean, and the scroll behavior predictable.
Performance Considerations
Virtualization improves performance mainly by reducing DOM node count. But there are still other things to consider. Scroll events fire frequently, so calculations should be lightweight. Expensive row rendering logic may still hurt performance even with virtualization. Memoization can help if item rendering is complex.
- Keep item rendering lightweight
- Use memoization when calculations grow complex
- Avoid expensive work in the scroll handler
- Use stable keys for rows
- Combine virtualization with API pagination when datasets are extremely large
Real-World Example
A messaging dashboard may need to show tens of thousands of chat messages, logs, or audit entries. Rendering all rows would hurt performance badly. A virtualized list lets the application behave like a complete long feed while only mounting a small visible window. This makes the UI faster and more memory efficient.
The same pattern is used in admin tables, financial ledgers, analytics viewers, IDE side panels, and many enterprise dashboards where large datasets are common.
What Changes When Row Heights Are Dynamic?
Fixed-height virtualization is the common interview version because calculations are straightforward. Dynamic-height rows make the problem harder because the component can no longer calculate the visible index using simple division. It must measure actual row heights or maintain a height map and cumulative offsets.
That version is more advanced and usually solved using specialized libraries or more complex measurement logic. In most machine coding rounds, solving fixed-height virtualization cleanly is enough.
When to Build vs When to Use a Library
In interviews, the goal is to show understanding, so building a simple virtualized list manually is valuable. In production applications, libraries such as react-window or react-virtualized are often used because they handle many edge cases and provide battle-tested APIs.
Knowing how the technique works internally is important even if a library is used later.
Final Takeaway
A virtualized list is a classic frontend performance problem. The goal is not just to render items, but to render only what the user needs at the moment. A good implementation preserves natural scrolling, calculates visible rows accurately, and keeps DOM size under control.
In machine coding rounds, this question stands out because it tests practical frontend engineering. A candidate who can explain the math clearly and implement a clean windowing solution demonstrates strong understanding of performance-aware UI development.