Virtualization
Only the rows and columns inside the viewport are rendered, so the DOM node count stops growing with the data. It is a windowing stage, which is why it replaces pagination rather than joining it.
Basic Usage
Two thousand rows below. Scroll it and watch the readout: the rendered window moves while the number of rows in the DOM stays put.
<!-- virtual replaces pagination rather than joining it, and the viewport
is what decides the window, so the grid needs a height. -->
<DataGrid data={rows} {columns} {getRowId} virtual class="h-[420px]" />
<script lang="ts">
import { createDataGrid, DataGrid, virtualization } from '@sv5ui/datagrid';
// The same thing with the feature registered by hand
const grid = createDataGrid<Person>({
data: rows,
columns,
getRowId,
features: [virtualization({ rowHeight: 40, overscan: 5 })]
});
</script>Options
A fixed height is the fast path: an offset is arithmetic rather than a lookup. Reach for the variable path only when the rows genuinely differ.
overscan is the one option you can watch. Both grids below hold the same 2,000 rows at the same height
and differ in that number alone: the first renders the window and nothing else, the second keeps
twenty rows in hand on each side. Scroll them and read the counts.
virtualization({
// Fixed height, the fast path: an offset is arithmetic, not a lookup.
rowHeight: 40,
// Rows rendered above and below the visible window.
overscan: 5,
// Rows rendered before the viewport has been measured, which is what
// SSR and the first paint show.
initialRows: 20,
// Renders only the columns intersecting the viewport, plus overscan.
columns: true,
// or: columns: { overscanPx: 200, initialColumns: 20 }
});Measured Rows
Every fourth row below carries a long note and asks for 'auto',
so it is measured and keeps its own height while the rest stay at 40 pixels.
// A per-row height, or 'auto' to size to content. Either one switches the
// virtualizer to its variable-height layout, which keeps offsets in a
// Fenwick tree: O(log n) per lookup rather than the fixed path's arithmetic.
virtualization({
rowHeight: 40,
getRowHeight: (node) => (node.row.notes.length > 60 ? 'auto' : 40)
});
// An 'auto' row renders at rowHeight for one frame, is measured, and keeps
// that height. The measurement is keyed by row id, so it survives sorting.Column Virtualization
Thirty columns, of which only the ones on screen are rendered. Name is pinned, so it stays while the rest scroll under it.
Before the viewport has been measured there is nothing to window by, which is every
server-rendered paint and the first client one. initialColumns bounds what is drawn until then, the way initialRows does for the row axis, and it matters more here: a grid of thousands of columns that renders
them all pays for every cell of every rendered row before anything is on screen. Raise it if
your columns are narrow enough that twenty leave a gap on first paint.
Once measured, the visible range is found by binary search rather than by walking from the first column, so scrolling far to the right costs the same as scrolling near the start.
The real world example takes this to the end of its rope: a control there hands the grid up to fifty thousand columns over ten million rows, and shows what moves and what does not. Measured there in a headless Chrome, the cells in the DOM are the same 161 at fifty thousand columns as at forty-eight, while swapping the list costs 195ms at twenty thousand and 929ms at fifty.
// Column virtualization is off by default: it pays for itself once a grid
// is wide, and costs a little where it is not.
virtualization({ columns: true });
// The overscan is in pixels either side of the viewport, not in columns,
// because a column's width is what decides how much work it is.
virtualization({ columns: { overscanPx: 200 } });
// Before the viewport is measured there is nothing to window by, which is
// every server-rendered paint. initialColumns is what is drawn until then,
// the row axis's initialRows for the other axis.
virtualization({ columns: { initialColumns: 20 } }); // the defaultScrolling to a Row
Both scrollToRow and ensureVisible take a row index or a row id, so a row you hold by id does not need to be found first. The badge
is the index of the first rendered row, which is how you can tell the two apart: scrollToRow moves the viewport whether or not it needs to, while ensureVisible does nothing when the row
is already on screen.
import { getVirtualization } from '@sv5ui/datagrid';
// Both take a row index or a row id.
getVirtualization(grid)?.scrollToRow(500); // puts the row at the top
getVirtualization(grid)?.ensureVisible('500'); // scrolls only if it is out of view
// Also on the flat api
grid.api.scrollToRow?.(500);
grid.api.ensureVisible?.(500);
// The rendered window itself
getVirtualization(grid)?.virtualizer.range; // { start, end }virtualization() Options
| Option | Default |
|---|---|
rowHeight | 40 |
getRowHeight | - |
overscan | 5 |
initialRows | 20 |
columns | false |
columns.initialColumns | 20 |
Virtualization State
What getVirtualization(grid) exposes. scrollToRow and ensureVisible are also on grid.api.
| Member | Description |
|---|---|
virtualizer.range | The slice of rows currently rendered |
virtualizer.totalHeight | Height of the whole list, which is what the scrollbar measures |
scrollToRow(target) | Row index or row id, put at the top of the viewport |
ensureVisible(target) | The same, but only scrolls when the row is out of view |
columnVirtualizer | Present only when column virtualization is on |