This episode dissects TanStack Virtual: the windowing concept and useVirtualizer, virtual scrolling for lists and tables, measuring dynamic items with measureElement, and performance tuning such as overscan for large data sets.

Rendering a list of one hundred thousand items is a punishment for the browser: every item needs a DOM node, and every scroll forces a layout pass. TanStack Virtual solves this with windowing — only the items visible in the viewport are actually rendered.
Episode 11 dissects the windowing concept and useVirtualizer, virtual scrolling for lists and tables, measuring items with dynamic heights, and performance tuning with overscan and size estimation.
This is the pattern that saves dashboard apps and log viewers from performance degradation. In episode 9 you already touched virtualized tables; now we go deep into the mechanics.
The Virtualizer computes which items fit in the viewport based on scroll position, then provides that range of items. The remaining space is filled with placeholders as tall as the total data:
import { useRef } from "react"
import { useVirtualizer } from "@tanstack/react-virtual"
function Daftar() {
const parentRef = useRef(null)
const virtualizer = useVirtualizer({
count: 100_000,
getScrollElement: () => parentRef.current,
estimateSize: () => 40,
})
const items = virtualizer.getVirtualItems()
return (
<div ref={parentRef} style={{ height: 400, overflowY: "auto" }}>
<div style={{ height: virtualizer.getTotalSize(), position: "relative" }}>
{items.map((item) => (
<div
key={item.key}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: item.size,
transform: `translateY(${item.start}px)`,
}}
>
Baris ke-{item.index}
</div>
))}
</div>
</div>
)
}getScrollElement: () => parentRef.current points to the scroll element. getVirtualItems() returns the visible items; each item is positioned with a translateY of its start value. getTotalSize() gives the total height so the scrollbar tracks all 100,000 rows even though only a dozen or so are rendered.
For tables, virtualization is usually applied to rows (as in episode 9) and can also apply to columns. Virtual rows take their data from table.getRowModel().rows, so sorting and filtering still run before virtualization:
const rows = table.getRowModel().rows
const virtualRows = virtualizer.getVirtualItems()
const totalSize = virtualizer.getTotalSize()
return (
<tbody>
{virtualRows.map((virtualRow) => {
const row = rows[virtualRow.index]
return (
<tr key={row.id} style={{ height: virtualRow.size }}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
)
})}
</tbody>
)virtualRows contains the visible rows, and each item maps to a real table row by index. The row model stays the source of truth, while the virtualizer decides what enters the DOM.
When items have different heights, static estimation isn't accurate. Use measureElement so the virtualizer measures real heights after render:
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 80,
measureElement: (el) => el.getBoundingClientRect().height,
})
<div
ref={virtualizer.measureElement}
data-index={item.index}
style={{ transform: `translateY(${item.start}px)` }}
>
{items[item.index]}
</div>ref={virtualizer.measureElement} is attached to each item along with data-index. After an item renders, the virtualizer measures its actual height and corrects the position of subsequent items. Scrolling becomes accurate even when every item's content differs in length.
Two key settings for performance:
overscan: the number of extra items rendered outside the viewport. Small values trim React's workload; large values prevent flash during fast scrolling.estimateSize: use an estimate close to the real size so initial position calculations are accurate.const virtualizer = useVirtualizer({
count: 100_000,
getScrollElement: () => parentRef.current,
estimateSize: () => 40,
overscan: 8,
scrollMargin: parentRef.current?.offsetTop ?? 0,
})overscan: 8 renders eight extra items in each direction. scrollMargin matters when the scroll area doesn't start at the top of the page — without it, item positions are wrong when there are elements above the container.
Warning
Don't attach ref={virtualizer.measureElement} to components that frequently change size, like slowly loading images. Measure with a separate ResizeObserver so the virtualizer isn't re-measured endlessly.
Episode 11 wrapped up virtualization: windowing with useVirtualizer that only renders visible items, virtual scrolling for lists and tables, measuring dynamic items through measureElement, and tuning with overscan and realistic estimation.
Key takeaways:
In the next episode, episode 12, we'll discuss authentication and secure data fetching — secure token storage, auth headers on the query client, the refresh token flow, router guards, and role-based data fetching. Your data is starting to need protection!