Worker Row Model
The same data source, off the main thread. A columnar copy is loaded into a worker that filters and sorts on indices, and falls back to the main thread if the worker is lost.
serverRowModel() means "not this thread", and workerDataSource() is a DataSource,
so it drops into the slot that feature already takes. Nothing extra is registered. Fifty Thousand Rows, Sorted Elsewhere
Sort a column or type in the quick filter below. The work happens in a worker; the main thread only draws the window that comes back.
No data
import {
createDataGrid, filtering, serverRowModel, sorting,
virtualization, workerDataSource
} from '@sv5ui/datagrid';
const source = workerDataSource(readings, {
columns,
searchText: (value) => String(value ?? ''),
onFallback: (reason) => console.warn('back on the main thread:', reason)
});
const grid = createDataGrid<Reading>({
columns,
data: [],
getRowId: (row) => row.id,
rowModel: 'server',
features: [
sorting(),
filtering(),
virtualization({ rowHeight: 40, overscan: 6 }),
serverRowModel(source, {
mode: 'infinite',
blockSize: 200,
placeholder: (index) => ({
id: `pending-${index}`,
sensor: '',
site: '',
celsius: 0,
humidity: 0
})
})
]
});Where the Rows Come From
The grid above was handed an array the page had already built, which no application has. The rows come from a server, and a worker row model is not an alternative to one: it is what to do with the rows after they arrive.
This grid made one request for the whole working set, handed it to the worker with setRows,
and has not spoken to the server since. Sort it, filter it, search it: the log underneath
stays at one line, because every one of those is answered off the main thread from rows
that are already here.
That is the trade, and it is a real one. A server row model asks again on every sort and works at any size. This asks once and then answers instantly, for as long as the set fits in memory.
No data
The last few requests
- Loading the working set...
// A worker row model is not an alternative to a server. It is what to do
// with the rows once the server has handed them over: one download, then
// every sort and filter after it answered off the main thread.
const source = workerDataSource<Person>([], { columns });
const grid = createDataGrid<Person>({
columns, data: [], getRowId,
rowModel: 'server',
features: [
sorting(), filtering(),
virtualization({ rowHeight: 40, overscan: 8 }),
serverRowModel(source, { mode: 'infinite', blockSize: 200, placeholder })
]
});
// One request brings the working set down. setRows rebuilds the columnar
// store in chunks, so no single frame is spent on it.
const { rows } = await (await fetch('/api/people', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ startRow: 0, endRow: 5000 })
})).json();
source.setRows(rows);
getServerRowModel(grid)!.refresh();
// The trade is plain. A server row model asks again on every sort, and works
// at any size. This asks once, and then answers instantly for as long as the
// set fits in memory. Past a few hundred thousand rows, go back to the
// server; below that, this is one round trip instead of hundreds.How It Works
Three Steps
Sorting indices is the point: nothing is copied per comparison, and a row crosses the boundary once.
| Step | What happens |
|---|---|
A columnar copy | The rows are read once into one array per column, pulled through each column's accessor. That copy is what the worker holds |
Chunked in | The copy crosses in chunks, yielding to paint between them, so the first render is not held up by the load |
Indices, not rows | A query filters and sorts indices and sends back only the window the grid asked for, so nothing is copied per comparison |
const source = workerDataSource(readings, { columns });
source.usingWorker; // true until it falls back
source.setRows(nextRows); // replace the data and rebuild the copy
source.dispose(); // terminate the worker
// The rows are read once into one array per column, chunked across yielding
// to paint between chunks, and every query then filters and sorts indices
// rather than rows.Losing the Worker
If the worker errors it is terminated, the queries waiting on it are answered on the main
thread instead, and onFallback says why. The grid keeps working either way, which is exactly the problem: without wiring onFallback a page that has quietly lost its worker looks like a page that never had one.
let onWorker = $state(true);
const source = workerDataSource(readings, {
columns,
onFallback: (reason) => {
onWorker = false;
report(reason);
}
});
// inline skips the worker deliberately, for a test or an environment
// without one.
const local = workerDataSource(readings, { columns, inline: true });
local.usingWorker; // falseusingWorker for the same reason. What Cannot Cross
A function cannot be sent to a worker, and two column options are functions the query needs: sortFn and a filter.predicate of your own. A column carrying either has its query run on the main thread as well, so the worker
stops being a saving for it.
sortFn and predicate first, and prefer the built-in comparator or filter where it will do. const columns: ColumnDef<Reading>[] = [
{ id: 'site', header: 'Site', filter: 'set' },
// Both of these are functions, so the query for this column runs on the
// main thread as well as in the worker.
{ id: 'sensor', header: 'Sensor', sortFn: (a, b) => natural(a.sensor, b.sensor) },
{
id: 'celsius',
header: 'Celsius',
filter: { type: 'number', predicate: (value, row) => row.celsius > 0 }
},
// An accessor is fine: it is evaluated while the columnar copy is built.
{ id: 'fahrenheit', header: 'F', accessor: (row) => row.celsius * 1.8 + 32 }
];Options
workerDataSource(rows, options). The rows are the first argument, not an option.
| Option | Description |
|---|---|
columns | What to read into the columnar copy |
nulls | Where blanks land in a sort |
readValue | Read a value yourself. Return undefined to leave the cell to the grid |
searchText | The text a quick filter matches. Omit it and no text index is built, which is cheaper when nothing searches |
groupRow | The row a group stands for, under groupBy |
onFallback | Called when the worker is lost and the work returns to the main thread |
inline | Skip the worker deliberately, for a test or an environment without one |
What It Returns
A DataSource and a handle in one object.
| Member | What it does |
|---|---|
usingWorker | False once it has fallen back, or when inline was set |
setRows(rows) | Replace the data and rebuild the columnar copy |
dispose() | Terminate the worker |
getRows(request) | The DataSource half, which serverRowModel() calls. You never call it yourself |
Everything Else
workerDataSource(readings, {
columns,
nulls: 'last',
readValue: (row, column) =>
column.id === 'total' ? row.qty * row.price : undefined,
searchText: (value) => String(value ?? ''),
groupRow: (keys, count) => ({ id: keys.join('/'), name: keys.at(-1), count }),
onFallback: (reason) => report(reason),
inline: false
});