Virtualized Data Table
Machine-coding brief for a virtualized table — windowing math, sticky header, row height, scroll performance.
- machine-coding
- interview
- react
- performance
- tables
Problem statement
Build a virtualized data table that can render 10k–100k rows by mounting only the visible window. Interviewers score scroll math, spacer heights, sticky headers, and correctness under fast scrolling — not every spreadsheet feature.
Requirements
Must have
- Fixed row height (simpler) windowing
- Vertical scroll container with total height =
rows.length * rowHeight - Render only
startIndex…endIndex(+ overscan) - Sticky header columns labels
- Stable row keys
Should have
- Overscan of 5–10 rows
- Horizontal scroll for many columns (header sync)
- Loading placeholder rows when data is window-fetched
Nice to have
- Variable row heights (more complex measurement)
- Sticky first column
- Integrate sort/filter on full dataset indices
Planning (5 minutes out loud)
- Fixed height first — say variable heights are phase 2
- scrollTop → index via division
- Absolute position rows or padding spacers
- MVP — list virtualization; then table columns
- Don’t render 50k DOM nodes
Architecture
VirtualTable
├── Header (sticky)
└── Body scroller
├── top spacer / transform offset
├── visible rows
└── bottom spacer
Types
type VirtualTableProps<T> = {
rows: T[];
rowHeight: number;
height: number; // viewport
columns: { key: keyof T & string; header: string; width: number }[];
getRowId: (row: T) => string;
overscan?: number;
};
Implementation sketch
function VirtualTable<T>({
rows,
rowHeight,
height,
columns,
getRowId,
overscan = 5,
}: VirtualTableProps<T>) {
const [scrollTop, setScrollTop] = useState(0);
const total = rows.length * rowHeight;
const visibleCount = Math.ceil(height / rowHeight);
const start = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
const end = Math.min(rows.length, start + visibleCount + overscan * 2);
const offsetY = start * rowHeight;
return (
<div className="vtable" style={{ width: "100%" }}>
<div
className="vtable-header"
style={{
display: "flex",
position: "sticky",
top: 0,
zIndex: 1,
background: "var(--bg, #fff)",
}}
>
{columns.map((c) => (
<div key={c.key} style={{ width: c.width, flex: "none", fontWeight: 600 }}>
{c.header}
</div>
))}
</div>
<div
className="vtable-body"
style={{ height, overflow: "auto" }}
onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}
>
<div style={{ height: total, position: "relative" }}>
<div
style={{
position: "absolute",
top: offsetY,
left: 0,
right: 0,
}}
>
{rows.slice(start, end).map((row, i) => {
const index = start + i;
return (
<div
key={getRowId(row)}
role="row"
style={{
display: "flex",
height: rowHeight,
boxSizing: "border-box",
}}
aria-rowindex={index + 1}
>
{columns.map((c) => (
<div
key={c.key}
role="cell"
style={{ width: c.width, flex: "none", overflow: "hidden" }}
>
{String(row[c.key] ?? "")}
</div>
))}
</div>
);
})}
</div>
</div>
</div>
</div>
);
}
Scroll performance tip
// rAF throttle
const ticking = useRef(false);
function onScroll(e: React.UIEvent<HTMLDivElement>) {
const top = e.currentTarget.scrollTop;
if (ticking.current) return;
ticking.current = true;
requestAnimationFrame(() => {
setScrollTop(top);
ticking.current = false;
});
}
Variable height outline (verbal + sketch)
- Keep
offsetMap[i]prefix sums of heights - Binary search scrollTop → start index
- Measure mounted rows with ResizeObserver; patch heights → recompute offsets
- Much more code — flag as extension
Accessibility essentials
- Prefer real table semantics when possible; virtualization often uses
role="grid"/row/gridcell aria-rowcount={rows.length}on grid;aria-rowindexon rows- Sticky header still announced as column headers (
role="columnheader") - Keyboard: optional arrow row focus — hard; mention as follow-up
Performance notes
| Topic | Guidance |
|---|---|
| DOM nodes | O(visible + overscan) |
| React | Don’t key by index; pure row cells |
| Work | Throttle scroll state |
| Data | Window-fetch from server when data itself is huge |
| CSS | content-visibility can help non-virtual lists but isn’t a full substitute |
Footguns
- Incorrect total height → broken scrollbar
- No overscan → blank flash on fast scroll
- Index as key → state bleed when sorting
- setState every scroll event without rAF → jank
- Measuring layout in render → thrashing
Interview out-loud answer
Virtualization mounts only rows in the viewport. With fixed row height, start index is
floor(scrollTop / rowHeight), total height isn * rowHeight, and rows are offset with absolute positioning. Overscan hides blank gaps. Sticky header sits outside or above the scrolling body. Variable heights need prefix offsets and measurement — I’d only implement that if asked. Libraries like TanStack Virtual are production default.