gridcn1.3.0

Virtualization

Row and column virtualization: what renders, what stays fixed, and what you get for free.

Both axes are virtualized. Only the rows and columns that actually intersect the viewport, plus a small overscan buffer, ever mount as real DOM cells. This is what lets a 100,000 x 5 grid render and scroll like a 30-row one. See Performance for the headline numbers and the regression suite that enforces them.

On by default, no config

Virtualization is not a prop you opt into. It is how the grid always renders. There is nothing to configure to get it, and no override to render everything unvirtualized.

Give the grid a bounded height

Without a bounded height, the viewport's clientHeight covers the whole dataset, and virtualization cannot activate. A dev build shows this warning:

gridcn: the grid viewport shows more than {N} rows at once — its height is probably unbounded,
which disables virtualization. Give the grid a bounded height (e.g. className="h-150" or h-full
inside a sized parent). Rendering is capped at {N} rows.

To fix this, give the grid, or a sized parent, an explicit height, for example className="h-150" or h-full inside a container with its own bounded height.

Row limit

Browsers cap or ignore tall elements: Firefox at about 17.9 million pixels, Chromium at about 33.5 million. To work in every browser, keep the row count times the row height below 17.8 million pixels. This limits one grid to about 490,000 rows at the default row height of 36 px. A smaller row height allows more rows. A larger row height allows fewer. If the row count times the row height passes 17.8 million pixels, Firefox shows no scroll range at all. If it passes 33.5 million pixels, Chromium cannot reach the rows past about 930,000 at 36 px. A development build logs a warning when the grid crosses the limit.

How it works

The model, in consumer terms

The scrollable area is a sticky viewport. The rows and columns you see are absolutely positioned inside a full-size canvas, sized to the total row and column extent, so the scrollbar's length and thumb position are always correct. One translate on the rows canvas slides the rendered rows to track the scroll position. It updates on every tick, not on the next React commit. The browser never reveals a strip of unrendered area between a scroll event and the window catching up, even during a hard scrollbar-drag fling.

Rows that stay in the window keep their grid position. The grid origin trails the window and moves only after the window drifts far from it, so a scroll step does not rewrite every row.

Overscan

A small fixed overscan, rows and columns rendered past the visible edge, absorbs the normal gap between one scroll tick and the next window recompute. During a fast fling or a scrollbar drag, the grid tracks recent scroll velocity. It grows the overscan on the leading edge only.

A jump is a scroll farther than the viewport plus the largest velocity buffer. After a jump, the grid mounts only the base overscan. The velocity buffer applies to fling and drag deltas below that distance. A jump from the top to the middle of 100,000 rows therefore mounts one set of rendered rows, not that set plus a large buffer.

What's virtualized

  • Rows: only data rows that intersect the viewport, plus overscan, mount. Row height is uniform (rowHeight or density), so the window is a cheap range computation, not a per-row measurement pass.
  • Columns: only columns that intersect the horizontal viewport, plus overscan, mount, independent of row virtualization. A diagonal scroll recomputes both in the same tick.

Guarantees

Interaction with pinned rows and columns

Pinned-left and pinned-right columns, and pinned-top and pinned-bottom row bands (see Columns and Pinned rows), always render, regardless of scroll position. They sit outside the virtualized range by construction, not merely overscanned, so they never blank, flicker, or lose focus while scrolling. The unpinned window's viewport bounds shrink by the space the pinned bands and columns occupy.

What you get for free

  • There is no row or column count threshold to cross, and no "virtualize past N rows" toggle. Small and huge datasets go through the identical code path.
  • Keyboard navigation, useDataGridScrollToCell, and programmatic scroll all work with virtualized content. Moving the active cell off-window scrolls it back into the rendered range automatically, so you do not need to pre-render a wider window.
  • Resize, reorder, and pin or unpin recompute the window correctly with no manual invalidation.

Keep data/columns/callback references stable

Virtualization does not change the standard identity-stability rule: data, columns, getRowId, getRowClassName and getCellClassName, and pinned-row arrays must be module-scope constants or memoized. See Performance. A dev-mode guardrail warns when an identity changes across renders.

Beyond rendering: fetching data on demand

Virtualization only decides what renders. It does not fetch anything. The dataset itself can be too large or expensive to load up front, not only to render. For that case, see Lazy loading, an add-on built on the rendered-rows callback below. It fetches windows on demand and renders skeleton rows for rows that have not arrived yet.

onRenderedRowsChange?: (range: { start: number; end: number }) => void fires after a row-window commit, with the rendered [start, end) data-row range, the same range that this section describes. This callback exists for exactly this kind of add-on. It is also useful for analytics or prefetch outside lazy loading.

On this page