Skip to content

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):

ActionTime
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 valueGetter and valueFormatter are 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 shallowRef for rows. Deep reactivity of large arrays is expensive and not needed by the grid.
  • Keep columns and options stable (module constants or useMemo), and memoize render props like cellRenderers.
  • Set a rowId. Row objects are reused on data updates, which keeps the selection and is faster.
  • Use a fixed rowHeight for large data sets. Rows do not need to be measured then. Without rowHeight, rows are measured with a ResizeObserver, which allows content based row heights.
  • Avoid expensive valueGetter and valueFormatter functions. 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.

Released under the ISC License.