Virtualization
With virtualization the DOM holds only the rows in view, plus a few either side, however many rows the grid has. The demo below has 50,000 and no pager.
# |
|---|
import * as React from "react";
import { DataGrid, createDataGridColumns } from "@/components/ui/data-grid";
import { makePeople, PLAN_OPTIONS, STATUS_OPTIONS, type Person,} from "@/lib/data-grid-demo-data";
const col = createDataGridColumns<Person>();
const columns = col.columns([ col.rowNumber(), col.text("name", { header: "Name" }), col.text("email", { header: "Email", size: 240 }), col.enum("status", { header: "Status", options: STATUS_OPTIONS }), col.enum("plan", { header: "Plan", options: PLAN_OPTIONS }), col.text("team", { header: "Team" }), col.currency("mrr", { header: "MRR", currency: "USD" }), col.number("seats", { header: "Seats" }), col.percent("usage", { header: "Usage" }), col.date("joinedAt", { header: "Joined" }), col.text("country", { header: "Country" }),]);
/** 50,000 rows, no pager: only the rows in view are in the DOM. */export default function DataGridVirtualized({ rows = 50_000 }: { rows?: number }) { const data = React.useMemo(() => makePeople(rows, 11), [rows]); return ( <DataGrid key="virtualized" data={data} columns={columns} getRowId={(row) => row.id} pagination={false} virtualize height={520} /> );}Use it
Section titled “Use it”Give the grid a bounded height and turn the pager off; virtualize is "auto" by default and kicks
in at 200 rows.
<DataGrid data={rows} columns={columns} getRowId={(row) => row.id} pagination={false} height={520}/>virtualize="auto"(the default) virtualizes once the rows the body would render number 200 or more.virtualize(true) always does;virtualize={false}never does;virtualize={{ threshold: 500, overscan: 12 }}tunes auto.- A bounded height is required. The virtualizer needs a scroll container of known height: pass
height(a pixel number or any CSS length) orfillHeight, which sizes the grid to the bottom of its scroll parent, console-style. The all-in-one<DataGrid>turnsfillHeighton by itself when rows virtualize and you gave noheight; with your own layout, set one on<DataGridTable>. - The count that matters is the rows the body would render. With the pager on, that is one page,
so a 10-row page never virtualizes; virtualization is for
pagination={false}or a very large page size.
How it behaves
Section titled “How it behaves”- TanStack Virtual does the work:
useVirtualizerwith the current row height as the estimate,overscanrows beyond each edge (8 by default), andgetItemKeyset to the row id, so React keys and measured heights survive sorting and filtering. Each rendered row is measured, so a row that wraps its text takes its true height. Two padding rows, hidden from assistive technology, keep the scrollbar the right length. - What stays. The sticky header, pinned columns (same table, same
cells), row selection (by id), cell selection and the keyboard (moving focus to a row that is not
in the DOM scrolls the virtualizer to it first), inline editing, sorting, filters, search, grouping,
expanding, and export of every filtered row rather than the rendered ones.
aria-rowcountreports the real total, and each row itsaria-rowindex. - What to expect. Rendering cost is flat: the DOM holds about the viewport height divided by the row height, plus twice the overscan. What remains is TanStack Table’s row model: each sort, filter or search recomputes over the whole array, so the cost of a keystroke grows with the row count. The demo’s 50,000 rows feel immediate; 100,000 plain rows are comfortable. Beyond that, or with expensive accessors and many faceted columns, move the work to a server.
- Density changes the estimate and the measured heights follow. Striping uses the virtual index, so stripes stay consistent as you scroll.
- Row pinning is the one feature that weakens: pinned rows are part of the virtual list, so a row pinned to the top leaves the DOM when the top of the list scrolls out of the overscan. See Row pinning.
- Fit to content measures rendered rows only, so it sizes a column to the rows in view.
When to page on the server instead
Section titled “When to page on the server instead”Virtualization solves rendering, not data. If the rows don’t fit comfortably in the browser, arrive
from an API a page at a time, or need sorting and filtering over a set the client never holds in
full, keep the pager and let the server page, sort and filter. See
Server-side data. The two combine: a server page of 500 rows and
virtualize on the grid renders only the visible slice of that page.
Options
Section titled “Options”Grid options:
| Option | Type | Default | What it does |
|---|---|---|---|
virtualize |
boolean, "auto" or options |
"auto" |
When to render only the rows in view. The options object is { overscan?, threshold? }. |
virtualize.overscan |
number |
8 |
Rows rendered beyond the viewport on each side. |
virtualize.threshold |
number |
200 |
With "auto", virtualize when the row model has at least this many rows. |
pagination |
false or options |
{ pageSize: 10 } |
false shows every row with no pager, which is when virtualization matters. |
height |
number | string |
A fixed height for the scroll area. | |
fillHeight |
boolean |
false |
Fill from the grid’s top edge to the bottom of its scroll parent, at least 240 px. <DataGrid> turns it on when rows virtualize and there is no height. |
Read-only on the api:
| Property | What it holds |
|---|---|
grid.virtualizeRows |
Whether rows are virtualized right now. |
grid.virtualizeOptions |
The resolved { overscan, threshold }. |
See also
Section titled “See also”- Server-side data: when the rows should not all be in the browser.
- Pagination: the alternative for mid-sized sets.
- Row pinning: its limits with virtualization.