ESC

Type to search the knowledge base.

Virtualized Data Table

Machine-coding brief for a virtualized table — windowing math, sticky header, row height, scroll performance.

advanced4 min read
  • 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)

  1. Fixed height first — say variable heights are phase 2
  2. scrollTop → index via division
  3. Absolute position rows or padding spacers
  4. MVP — list virtualization; then table columns
  5. 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-rowindex on 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

  1. Incorrect total height → broken scrollbar
  2. No overscan → blank flash on fast scroll
  3. Index as key → state bleed when sorting
  4. setState every scroll event without rAF → jank
  5. 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 is n * 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.

Further reading