Home/Performance/Scrolling Performance
Performance

Scrolling Performance

What a scroll costs, and what keeps it smooth once your features are turned on. Sort, resize, reorder and select in the ten-thousand-row grid below — the number of rendered rows stays bounded wherever you are in the data.

Ten thousand rows with everything on

Sorting, column resize, column reorder and multi-row selection, all live on the scrolling grid rather than beside it. Scroll to the very bottom — the counter settles at a few dozen rows and stays there.

0 of 10,000 rows are in the DOM right now.

Code
Summary
Team
Priority
Points
GC-00001
Backfill missing invoices
Billing
Urgent
7
GC-00002
Retry the failed webhook
Infra
Low
13
GC-00003
Debounce the search box
Mobile
High
9
GC-00004
Backfill missing invoices
Infra
Normal
1
GC-00005
Rotate the signing key
Billing
High
11
GC-00006
Retry the failed webhook
Search
Low
2
GC-00007
Rotate the signing key
Billing
Normal
8
GC-00008
Backfill missing invoices
Billing
High
4
GC-00009
Backfill missing invoices
Search
Urgent
10
GC-00010
Rotate the signing key
Search
High
4
GC-00011
Cache the lookup table
Billing
Normal
8
GC-00012
Backfill missing invoices
Search
Low
2
GC-00013
Retry the failed webhook
Search
Urgent
11
GC-00014
Split the settings screen
Platform
High
9
GC-00015
Cache the lookup table
Mobile
Normal
6
GC-00016
Rotate the signing key
Data
High
6
GC-00017
Retry the failed webhook
Search
High
6
GC-00018
Rotate the signing key
Infra
Normal
9
GC-00019
Rotate the signing key
Infra
High
4
GC-00020
Debounce the search box
Billing
Low
1
GC-00021
Debounce the search box
Infra
Normal
3
GC-00022
Cache the lookup table
Infra
Low
3
GC-00023
Backfill missing invoices
Data
High
2
GC-00024
Backfill missing invoices
Infra
Urgent
1
GC-00025
Debounce the search box
Infra
High
9
GC-00026
Cache the lookup table
Data
Urgent
5
80 / 10,000 records

A row that leaves and comes back is the same row

Select a few rows, then scroll far enough that they are gone from the page entirely and scroll back. They are still selected — the grid holds selection against the row, not against the element that was drawing it.

Code
Summary
Team
Points
GC-00001
Backfill missing invoices
Billing
7
GC-00002
Retry the failed webhook
Infra
13
GC-00003
Debounce the search box
Mobile
9
GC-00004
Backfill missing invoices
Infra
1
GC-00005
Rotate the signing key
Billing
11
GC-00006
Retry the failed webhook
Search
2
GC-00007
Rotate the signing key
Billing
8
GC-00008
Backfill missing invoices
Billing
4
GC-00009
Backfill missing invoices
Search
10
GC-00010
Rotate the signing key
Search
4
GC-00011
Cache the lookup table
Billing
8
GC-00012
Backfill missing invoices
Search
2
GC-00013
Retry the failed webhook
Search
11
GC-00014
Split the settings screen
Platform
9
GC-00015
Cache the lookup table
Mobile
6
GC-00016
Rotate the signing key
Data
6
GC-00017
Retry the failed webhook
Search
6
GC-00018
Rotate the signing key
Infra
9
GC-00019
Rotate the signing key
Infra
4
GC-00020
Debounce the search box
Billing
1
GC-00021
Debounce the search box
Infra
3
GC-00022
Cache the lookup table
Infra
3
GC-00023
Backfill missing invoices
Data
2
GC-00024
Backfill missing invoices
Infra
1
GC-00025
Debounce the search box
Infra
9
GC-00026
Cache the lookup table
Data
5
80 / 10,000 records

Row height and the window

The same ten thousand rows at two row heights. The shorter rows fit more of the viewport, so more of them are rendered — and both counts stay bounded.

rowHeight: 28

0 rows in the DOM

Code
Team
Priority
GC-00001
Billing
Urgent
GC-00002
Infra
Low
GC-00003
Mobile
High
GC-00004
Infra
Normal
GC-00005
Billing
High
GC-00006
Search
Low
GC-00007
Billing
Normal
GC-00008
Billing
High
GC-00009
Search
Urgent
GC-00010
Search
High
GC-00011
Billing
Normal
GC-00012
Search
Low
GC-00013
Search
Urgent
GC-00014
Platform
High
GC-00015
Mobile
Normal
GC-00016
Data
High
GC-00017
Search
High
GC-00018
Infra
Normal
GC-00019
Infra
High
GC-00020
Billing
Low
GC-00021
Infra
Normal
GC-00022
Infra
Low
GC-00023
Data
High
GC-00024
Infra
Urgent
GC-00025
Infra
High
GC-00026
Data
Urgent
GC-00027
Infra
Normal
GC-00028
Billing
High
GC-00029
Search
Urgent
GC-00030
Platform
Urgent
GC-00031
Billing
High
80 / 10,000 records

rowHeight: 64

0 rows in the DOM

Code
Team
Priority
GC-00001
Billing
Urgent
GC-00002
Infra
Low
GC-00003
Mobile
High
GC-00004
Infra
Normal
GC-00005
Billing
High
GC-00006
Search
Low
GC-00007
Billing
Normal
GC-00008
Billing
High
GC-00009
Search
Urgent
GC-00010
Search
High
GC-00011
Billing
Normal
GC-00012
Search
Low
GC-00013
Search
Urgent
GC-00014
Platform
High
GC-00015
Mobile
Normal
GC-00016
Data
High
GC-00017
Search
High
GC-00018
Infra
Normal
GC-00019
Infra
High
GC-00020
Billing
Low
GC-00021
Infra
Normal
GC-00022
Infra
Low
GC-00023
Data
High
80 / 10,000 records

What stays true while you scroll

Two properties hold at any dataset size and on any machine, which is why they are the ones worth stating: the number of rendered rows stays bounded, and a scroll step costs the browser at most one layout. Neither is a timing figure — both are properties of the work being done rather than measurements of how long it took.

Turning features on does not change either of them. The first grid above is sorting, resizing, reordering and selecting while it scrolls, and its counter behaves exactly like one on a bare grid.

Scrolling with the keyboard

Page Down and Page Up move a viewport at a time, Ctrl+End jumps to the last row and Ctrl+Home back to the first. They travel the same window the mouse does — a keyboard jump to the end of ten thousand rows renders the same handful of rows a drag to the end would.

This is on by default. Set keyboardNav: false only if your application wants those keys for itself.

Rows come back the way you left them

A row that scrolls out of view is removed from the page, and its element is reused for a row further down. That is what keeps the DOM small — and it is the thing readers are right to be nervous about, because a grid that recycles carelessly loses whatever you had done to the row.

It does not. Select a few rows in the second grid above, scroll far enough that they have left the page entirely, then scroll back: they are still selected. Everything the grid holds per row — selection, expansion, focus — is keyed to the row, never to the element that happened to be drawing it, so recycling an element cannot take any of it with it.

trackBy is what keeps that answer stable when your data changes rather than your scroll position. Without it a row is identified by the object itself, which holds up for as long as the objects do — and stops the moment a reload hands the grid a fresh array of equal-but-different rows. Set it, and a row stays the same row across a refetch too.

Taller rows, smaller window

The window is derived from the viewport, so row height decides how many rows fit inside it. Doubling rowHeight roughly halves the rendered count; it does not change the fact that the count is bounded. The third example is the same ten thousand rows at two heights, each with its own counter.

Nothing is left behind

Grids are mounted and destroyed constantly in a real application — a route change, a tab, a dialog. Seven interaction scenarios are each put through seven mount, use and destroy cycles on every build, and the result is zero grid instances still alive afterwards, with no listeners and no detached nodes left over.

Those are zeroes rather than sizes on purpose: a zero means the same thing on every machine, which is not true of a memory figure.

What decides the window

PropertyTypeDefaultDescription
config.heightnumber | string—The grid needs a height before it has a viewport to window — see Sizing & Height.
config.rowHeightnumber—Decides how many rows fit the viewport, and therefore how large the rendered window is.
config.trackBystring | ((row: T) => unknown)—Row identity. Recycling survives without it — a row is then identified by the object itself. Set it so identity also survives your data being replaced — see Row Identity.
config.keyboardNavbooleantrueArrow keys, Page Up / Page Down and Ctrl+Home / End move through the same window. Set false to hand those keys back to your application.
config.columnReorderbooleanfalseGrid-level switch for drag-to-reorder. A column's own draggable does nothing without it.
config.columnResizebooleanfalseGrid-level switch for drag-to-resize. A column's own resizable does nothing without it.
config.rowSelection{ mode: 'single' | 'multi' }—Selection is held per row, not per rendered element, so it survives the row leaving and re-entering the viewport.