Capítulo 46 de 54
Virtualization is not a TanStack Table feature at all — it's a separate concern handled by @tanstack/react-virtual, which decides which row/column indexes to render for the current scroll position while the table continues to own row models, columns, sizing, sorting, and filtering; your renderer's only job is mapping virtual indexes back to table.getRowModel().rows / table.getVisibleLeafColumns().
tableFeatures()/useTable() as any other table.useVirtualizer({ count: rows.length, getScrollElement, estimateSize, overscan }) → give the <tbody> height: rowVirtualizer.getTotalSize() + position: relative → render only rowVirtualizer.getVirtualItems(), each row absolutely positioned via transform: translateY(virtualRow.start).virtualColumns[0]?.start and columnVirtualizer.getTotalSize() - lastVirtualColumn.end — this preserves scroll width while keeping row-like table markup intact (which dynamic row-height measurement depends on). Configure with horizontal: true and estimateSize: (i) => visibleColumns[i].getSize().rows/visibleColumns lists — recompute after any sorting/filtering/pagination/grouping/visibility change, never reuse stale index mappings.data, virtualize the loaded rows, and trigger the next fetch when scroll position nears the bottom (scrollHeight - scrollTop - clientHeight < threshold). If sorting is server-driven, use manualSorting so a full re-sort re-fetches from the top — and call rowVirtualizer.scrollToIndex(0) when the sort changes and the loaded dataset gets replaced.estimateSize is just the virtualizer's initial guess; attach ref={(node) => rowVirtualizer.measureElement(node)} and data-index={virtualRow.index} on each row to let the virtualizer refine actual heights after render. Skip measureElement entirely when every row has a known fixed height — it's pure overhead in that case. More overscan reduces blank-region flashes while measurements settle, at the cost of more DOM nodes.table { display: grid }, thead { display: grid; position: sticky; top: 0 }, tr { display: flex } — native table layout doesn't cooperate with independently-positioned, variable-height virtual rows.overscan deliberately (blanking vs. DOM node count trade-off); avoid expensive cell renderers at large scale; measure with production builds (React dev mode is meaningfully slower); prefer fixed row sizes when the UI allows it; use column.getSize()/header.getSize()/cell.column.getSize() consistently for column virtualization sizing.onChange callback to imperatively mutate DOM styles (rowRef.style.transform, container CSS custom properties, body height) outside React's render flow entirely, paired with a custom React.memo comparator to freeze row/column components during scroll. Scope imperative DOM writes strictly to scroll-position-only styling (transform, sizes, spacer widths) — never route business/table state, data, sorting, or filtering through this path.const rows = table.getRowModel().rows
const rowVirtualizer = useVirtualizer({
count: rows.length, getScrollElement: () => containerRef.current, estimateSize: () => 33, overscan: 5,
})
<tbody style={{ height: rowVirtualizer.getTotalSize(), position: 'relative' }}>
{rowVirtualizer.getVirtualItems().map((vr) => {
const row = rows[vr.index]
return (
<tr key={row.id} style={{ position: 'absolute', transform: `translateY(${vr.start}px)`, width: '100%' }}>
{row.getVisibleCells().map((cell) => <td key={cell.id}>{/* ... */}</td>)}
</tr>
)
})}
</tbody>
virtualRow.start, mapping virtualRow.index back to the table's real row array.@tanstack/react-virtual (or another library) layered on top, reading table.getRowModel().rows/getVisibleLeafColumns().table.getRowModel().rows, the source list every virtualizer reads from.getSize() APIs used to size virtualized columns.