Appearance
Performance
The grid renders only the visible rows and columns (plus a small buffer), so the number of DOM elements does not depend on the size of the data.
vue
<template>
<div class="demo">
<div class="demo-toolbar">{{ ROWS.toLocaleString() }} rows × {{ COLUMNS }} columns, created in {{ duration }} ms</div>
<Datagrid :columns="columns" :rows="rows" :options="options" />
</div>
</template>
<script setup lang="ts">
import { shallowRef } from "vue";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
const COLUMNS = 50;
const ROWS = 100_000;
const start = performance.now();
const columns: Array<ColumnConfig> = Array.from({ length: COLUMNS }, (_, i) => ({
field: `c${i}`,
text: `Column ${i}`,
type: i % 3 === 0 ? "number" : undefined,
pinned: i === 0 ? "left" : undefined,
}));
// shallowRef: vue does not need to make every row reactive
const rows = shallowRef(
Array.from({ length: ROWS }, (_, row) => {
const data: Record<string, unknown> = {};
for (let i = 0; i < COLUMNS; i++) {
data[`c${i}`] = i % 3 === 0 ? (row * 7 + i * 13) % 1000 : `R${row} C${i}`;
}
return data;
})
);
const duration = Math.round(performance.now() - start);
const options: GridOptions = {
// fixed row heights are faster than measured row heights
rowHeight: 32,
selection: "Cell",
singleSelect: false,
defaultColumn: { sortable: true, filterable: true, resizeable: true, width: 120 },
};
</script>tsx
import { useState } from "react";
import { Datagrid, type ColumnConfig, type GridOptions } from "@datagrid/react-ui";
const COLUMNS = 50;
const ROWS = 100_000;
const columns: Array<ColumnConfig> = Array.from({ length: COLUMNS }, (_, i) => ({
field: `c${i}`,
text: `Column ${i}`,
type: i % 3 === 0 ? "number" : undefined,
pinned: i === 0 ? "left" : undefined,
}));
const options: GridOptions = {
// fixed row heights are faster than measured row heights
rowHeight: 32,
selection: "Cell",
singleSelect: false,
defaultColumn: { sortable: true, filterable: true, resizeable: true, width: 120 },
};
function createRows() {
const start = performance.now();
const rows = Array.from({ length: ROWS }, (_, row) => {
const data: Record<string, unknown> = {};
for (let i = 0; i < COLUMNS; i++) {
data[`c${i}`] = i % 3 === 0 ? (row * 7 + i * 13) % 1000 : `R${row} C${i}`;
}
return data;
});
return { rows, duration: Math.round(performance.now() - start) };
}
export default function BigData() {
// created once per mount, the array must keep its identity between renders
const [{ rows, duration }] = useState(createRows);
return (
<div className="demo">
<div className="demo-toolbar">
{ROWS.toLocaleString()} rows × {COLUMNS} columns, created in {duration} ms
</div>
<Datagrid columns={columns} rows={rows} options={options} />
</div>
);
}Benchmarks
Timings of the data pipeline for 100 000 rows (headless core, Node.js on a desktop CPU, fastest of 5 runs; each action is applied and reset):
| Action | Time |
|---|---|
| initialize | ~55 ms |
| sort by a number / date / text column | ~100 / ~150 / ~250 ms |
| column filter (text, number, date, set) | ~25–30 ms |
| quick filter, first search / each further keystroke | ~25 / ~15 ms |
| group by two columns with a sum | ~90 ms |
| select all rows and read the selected data | ~80 ms |
| CSV export | ~150 ms |
In the browser, scrolling 100 000 rows × 50 columns runs at 60 frames per second, also with the React component. Run the benchmark yourself with pnpm --filter @datagrid/core benchmark (ROWS=1000000 for other sizes).
How the core keeps these times low:
- Sorting reads every value once and converts it into a numeric key (numbers, timestamps, or the rank of a text among the distinct texts), so the 1.7 million comparisons of 100 000 rows only compare numbers. Plain alphanumeric texts are ranked without the slow
Intl.Collator. - Plain fields are read directly, only the results of
valueGetterandvalueFormatterare cached. - The quick filter caches a search text per row, so typing only searches strings.
- Rows scrolled into view are rendered in the same frame, also in React (scroll updates are flushed synchronously).
Tips
- Use
shallowReffor rows. Deep reactivity of large arrays is expensive and not needed by the grid.
- Keep
columnsandoptionsstable (module constants oruseMemo), and memoize render props likecellRenderers.
- Set a
rowId. Row objects are reused on data updates, which keeps the selection and is faster. - Use a fixed
rowHeightfor large data sets. Rows do not need to be measured then. WithoutrowHeight, rows are measured with aResizeObserver, which allows content based row heights. - Avoid expensive
valueGetterandvalueFormatterfunctions. Their results are cached per cell, but they are called again after data changes. - Prefer formatters over slots and components for plain text. Slots and components are more expensive to render.
- Use pagination or server side data for data sets which are too large to be loaded into the browser.
- Update the data with a new array instead of mutating many rows and calling
refresh()for each change.
Virtualization options
ts
virtualization: {
bufferRow: 1, // additional rows rendered above and below the viewport
bufferColumn: 1, // additional columns rendered left and right
}Columns with autoHeight are always rendered (because they define the height of the row), so keep their number small.