Rows

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.

2000 rows in the grid window 0 to 20 20 rows rendered
Hoang Kowalski
hoang.kowalski1@example.com
Design
Manager
$127,691.00
Bruno Nguyen
bruno.nguyen2@example.com
Growth
Support
$105,146.00
Bruno Dubois
bruno.dubois3@example.com
Design
Manager
$86,538.00
Farid Haddad
farid.haddad4@example.com
Growth
Analyst
$76,443.00
Jonas Yilmaz
jonas.yilmaz5@example.com
Core
Support
$129,812.00
Quyen Tanaka
quyen.tanaka6@example.com
Growth
Analyst
$72,011.00
Mai Nguyen
mai.nguyen7@example.com
Support
Analyst
$83,226.00
Bruno Novak
bruno.novak8@example.com
Support
Analyst
$71,507.00
Grace Dubois
grace.dubois9@example.com
Support
Manager
$94,212.00
Tuan Ivanov
tuan.ivanov10@example.com
Design
Engineer
$105,520.00
Bruno Andersen
bruno.andersen11@example.com
Growth
Support
$62,389.00
Bruno Yilmaz
bruno.yilmaz12@example.com
Core
Designer
$139,212.00
Grace Costa
grace.costa13@example.com
Platform
Support
$96,734.00
Niko Muller
niko.muller14@example.com
Core
Manager
$76,769.00
Olga Yilmaz
olga.yilmaz15@example.com
Platform
Analyst
$66,068.00
Dara Okafor
dara.okafor16@example.com
Platform
Support
$87,888.00
Quyen Silva
quyen.silva17@example.com
Design
Manager
$121,817.00
Grace Haddad
grace.haddad18@example.com
Support
Designer
$137,633.00
Keiko Novak
keiko.novak19@example.com
Core
Analyst
$125,071.00
Tuan Dubois
tuan.dubois20@example.com
Growth
Engineer
$73,645.00
2,000 rows
<!-- 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.

overscan 0, 20 rendered
Name
Email
Team
Hoang Kowalski
hoang.kowalski1@example.com
Design
Bruno Nguyen
bruno.nguyen2@example.com
Growth
Bruno Dubois
bruno.dubois3@example.com
Design
Farid Haddad
farid.haddad4@example.com
Growth
Jonas Yilmaz
jonas.yilmaz5@example.com
Core
Quyen Tanaka
quyen.tanaka6@example.com
Growth
Mai Nguyen
mai.nguyen7@example.com
Support
Bruno Novak
bruno.novak8@example.com
Support
Grace Dubois
grace.dubois9@example.com
Support
Tuan Ivanov
tuan.ivanov10@example.com
Design
Bruno Andersen
bruno.andersen11@example.com
Growth
Bruno Yilmaz
bruno.yilmaz12@example.com
Core
Grace Costa
grace.costa13@example.com
Platform
Niko Muller
niko.muller14@example.com
Core
Olga Yilmaz
olga.yilmaz15@example.com
Platform
Dara Okafor
dara.okafor16@example.com
Platform
Quyen Silva
quyen.silva17@example.com
Design
Grace Haddad
grace.haddad18@example.com
Support
Keiko Novak
keiko.novak19@example.com
Core
Tuan Dubois
tuan.dubois20@example.com
Growth
2,000 rows
overscan 20, 20 rendered
Name
Email
Team
Hoang Kowalski
hoang.kowalski1@example.com
Design
Bruno Nguyen
bruno.nguyen2@example.com
Growth
Bruno Dubois
bruno.dubois3@example.com
Design
Farid Haddad
farid.haddad4@example.com
Growth
Jonas Yilmaz
jonas.yilmaz5@example.com
Core
Quyen Tanaka
quyen.tanaka6@example.com
Growth
Mai Nguyen
mai.nguyen7@example.com
Support
Bruno Novak
bruno.novak8@example.com
Support
Grace Dubois
grace.dubois9@example.com
Support
Tuan Ivanov
tuan.ivanov10@example.com
Design
Bruno Andersen
bruno.andersen11@example.com
Growth
Bruno Yilmaz
bruno.yilmaz12@example.com
Core
Grace Costa
grace.costa13@example.com
Platform
Niko Muller
niko.muller14@example.com
Core
Olga Yilmaz
olga.yilmaz15@example.com
Platform
Dara Okafor
dara.okafor16@example.com
Platform
Quyen Silva
quyen.silva17@example.com
Design
Grace Haddad
grace.haddad18@example.com
Support
Keiko Novak
keiko.novak19@example.com
Core
Tuan Dubois
tuan.dubois20@example.com
Growth
2,000 rows
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.

Name
Team
Notes
Hoang Kowalski
Design
Joined from the platform team, currently splitting time between the design system and the reporting stack, and on call every third week.
Bruno Nguyen
Growth
Short note.
Bruno Dubois
Design
Short note.
Farid Haddad
Growth
Short note.
Jonas Yilmaz
Core
Joined from the platform team, currently splitting time between the design system and the reporting stack, and on call every third week.
Quyen Tanaka
Growth
Short note.
Mai Nguyen
Support
Short note.
Bruno Novak
Support
Short note.
Grace Dubois
Support
Joined from the platform team, currently splitting time between the design system and the reporting stack, and on call every third week.
Tuan Ivanov
Design
Short note.
Bruno Andersen
Growth
Short note.
Bruno Yilmaz
Core
Short note.
Grace Costa
Platform
Joined from the platform team, currently splitting time between the design system and the reporting stack, and on call every third week.
Niko Muller
Core
Short note.
Olga Yilmaz
Platform
Short note.
Dara Okafor
Platform
Short note.
Quyen Silva
Design
Joined from the platform team, currently splitting time between the design system and the reporting stack, and on call every third week.
Grace Haddad
Support
Short note.
Keiko Novak
Core
Short note.
Tuan Dubois
Growth
Short note.
200 rows
// 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.

Name
Metric 1
Metric 2
Metric 3
Metric 4
Metric 5
Metric 6
Metric 7
Metric 8
Metric 9
Metric 10
Metric 11
Metric 12
Metric 13
Metric 14
Metric 15
Metric 16
Metric 17
Metric 18
Metric 19
Hoang Kowalski
400
300
800
100
300
1100
500
1100
1100
1100
400
1700
1100
1100
1100
300
1800
1100
1600
Bruno Nguyen
600
200
800
600
800
200
200
600
1100
1000
100
800
0
600
2000
800
1300
200
2100
Bruno Dubois
400
200
300
800
100
600
1000
400
300
1000
800
1200
1200
1800
1800
1200
1200
1800
1300
Farid Haddad
300
300
600
300
400
300
300
300
300
1100
1100
1500
600
300
300
1500
1400
300
1800
Jonas Yilmaz
400
400
500
200
100
800
700
400
200
400
0
1400
400
1200
1100
1200
0
2000
1200
Quyen Tanaka
200
300
200
100
500
1100
400
900
1100
1100
1600
1100
100
1100
200
500
2100
1100
1100
Mai Nguyen
300
200
300
600
0
600
0
1000
600
1000
1100
1200
600
600
300
0
1200
1800
100
Bruno Novak
200
300
200
700
700
1100
700
900
200
300
500
1100
1000
700
200
700
0
1100
700
Grace Dubois
600
400
0
200
800
0
100
600
1200
400
1500
0
1000
1200
600
800
400
1200
1200
Tuan Ivanov
200
0
400
0
800
400
1200
200
1000
0
100
400
1300
0
1600
800
1900
1600
2000
Bruno Andersen
500
500
100
900
800
100
200
500
400
500
1600
100
1200
900
1900
1900
1300
1300
1400
Bruno Yilmaz
300
400
0
200
700
0
800
1000
1200
1200
1600
0
1800
1200
300
1800
1600
1200
1200
Grace Costa
100
600
200
400
0
200
100
800
1400
1400
400
200
500
1400
800
0
1900
1400
900
Niko Muller
0
100
800
900
0
500
400
700
1400
100
1400
1700
900
900
1400
1100
1800
1700
1900
Olga Yilmaz
200
400
800
800
200
800
200
200
800
400
600
800
500
800
200
200
1200
2000
1800
Dara Okafor
300
0
300
800
900
0
800
1000
300
0
1500
1200
1300
800
300
2000
500
0
1300
Quyen Silva
300
100
200
700
300
500
700
300
200
900
1200
1100
800
1700
1700
300
900
1700
1700
Grace Haddad
600
100
500
300
100
500
200
1300
800
100
100
500
1600
1300
2000
100
100
1700
800
Keiko Novak
200
700
700
100
100
700
1100
900
100
1500
200
700
1300
1100
1600
100
2000
700
2100
Tuan Dubois
500
500
700
500
0
100
0
500
1000
1300
100
700
100
500
1900
1100
2200
1300
2000
500 rows
// 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 default

Scrolling 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.

first rendered row 0
Name
Email
Team
Role
Salary
Hoang Kowalski
hoang.kowalski1@example.com
Design
Manager
$127,691.00
Bruno Nguyen
bruno.nguyen2@example.com
Growth
Support
$105,146.00
Bruno Dubois
bruno.dubois3@example.com
Design
Manager
$86,538.00
Farid Haddad
farid.haddad4@example.com
Growth
Analyst
$76,443.00
Jonas Yilmaz
jonas.yilmaz5@example.com
Core
Support
$129,812.00
Quyen Tanaka
quyen.tanaka6@example.com
Growth
Analyst
$72,011.00
Mai Nguyen
mai.nguyen7@example.com
Support
Analyst
$83,226.00
Bruno Novak
bruno.novak8@example.com
Support
Analyst
$71,507.00
Grace Dubois
grace.dubois9@example.com
Support
Manager
$94,212.00
Tuan Ivanov
tuan.ivanov10@example.com
Design
Engineer
$105,520.00
Bruno Andersen
bruno.andersen11@example.com
Growth
Support
$62,389.00
Bruno Yilmaz
bruno.yilmaz12@example.com
Core
Designer
$139,212.00
Grace Costa
grace.costa13@example.com
Platform
Support
$96,734.00
Niko Muller
niko.muller14@example.com
Core
Manager
$76,769.00
Olga Yilmaz
olga.yilmaz15@example.com
Platform
Analyst
$66,068.00
Dara Okafor
dara.okafor16@example.com
Platform
Support
$87,888.00
Quyen Silva
quyen.silva17@example.com
Design
Manager
$121,817.00
Grace Haddad
grace.haddad18@example.com
Support
Designer
$137,633.00
Keiko Novak
keiko.novak19@example.com
Core
Analyst
$125,071.00
Tuan Dubois
tuan.dubois20@example.com
Growth
Engineer
$73,645.00
2,000 rows
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

OptionDefault
rowHeight40
getRowHeight-
overscan5
initialRows20
columnsfalse
columns.initialColumns20

Virtualization State

What getVirtualization(grid) exposes. scrollToRow and ensureVisible are also on grid.api.

MemberDescription
virtualizer.rangeThe slice of rows currently rendered
virtualizer.totalHeightHeight 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
columnVirtualizerPresent only when column virtualization is on