Lattice Grid Buy a licence

demo D53

Sort at a million rows

Timed on the page, and honest about where it runs

measured, main thread

Building…
Loading a live grid…

The configuration

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.27.0/lattice-grid.min.css">

<script type="module">
  import { defineLatticeGrid } from 'https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.27.0/modules/webcomponent.esm.js';
  defineLatticeGrid();
</script>

<lattice-grid row-key="id" style="display: block; height: 540px"></lattice-grid>
<div id="out" style="margin-top: 10px; font: 12.5px system-ui"></div>

<script type="module">
  const el = document.querySelector('lattice-grid');
  el.config = {
    selection: 'multiple',
    // A large sort can move off the main thread onto a worker; filtering and
    // grouping never do, and a numeric column costs a fraction of a text one
    // because it compares numbers rather than collating strings.
    useWorker: true,
    workerThreshold: 50000,
    toolPanel: {
      side: 'left',
      panels: ['columns', 'filters', 'views', 'quick'],
      actions: ['undo', 'redo', 'export', 'restore', 'maximise'],
      exportName: 'lattice-demo',
    },
    columns: [
      { field: 'account', title: 'Account', filter: { type: 'set' } },
      { field: 'service', title: 'Service', filter: { type: 'set' } },
      { field: 'resource', title: 'Resource', layout: { flex: 1, min: 200, max: 320 } },
      { field: 'region', title: 'Region', filter: { type: 'set' } },
      { field: 'environment', title: 'Env', filter: { type: 'set' },
        cell: { decoration: 'pill', variant: { map: {
          prod: 'danger', 'prod-2': 'danger', 'definitely-prod': 'danger',
          staging: 'warning', untagged: 'warning', test: 'info',
          dev: 'success', 'not-prod': 'neutral',
        } } } },
      { field: 'change', title: 'Change', type: 'number',
        layout: { width: 140, min: 140 }, format: { style: 'percent', decimals: 1 } },
      { field: 'cost', title: 'Monthly cost', type: 'number',
        layout: { width: 170, pin: 'end' },
        format: { style: 'currency', currency: 'USD', decimals: 2 }, total: 'sum' },
    ],
  };
  el.rows = rows;  // one million cloud cost records

  // el.grid is the live grid inside the element, and reports its own account of
  // what it did. Sort a column and read whether that run moved off the main
  // thread.
  const out = document.getElementById('out');
  const render = () => {
    const w = el.grid.diagnostics?.renders?.()?.worker;
    out.textContent = !w || !w.spawned
      ? 'sort a column to see whether this run moved off the main thread'
      : `worker thread: ${w.remote} operation${w.remote === 1 ? '' : 's'} offloaded, ${w.local} stayed on the main thread (threshold ${w.threshold.toLocaleString('en-GB')} rows)`;
  };
  el.grid.on('sort:changed', render);
  render();
</script>

Sorting a million rows without freezing the tab

Sorting is one of the first places a data grid’s architecture shows through: a naive implementation reorders the DOM row by row, while a column-oriented one reorders an index array and lets virtual scrolling redraw only the rows in view. A developer reaches for this demo when a column click on a large dataset needs to stay responsive rather than freeze the tab, and when the honest answer to “how does this behave at scale” matters more than a synthetic benchmark. Lattice Grid measures the operation in place, reported through the panel on the page rather than a claim made in isolation. Clicking a header applies sort.set([{ col, dir }]), the same call used for a hundred rows, and the index rebuild is what changes size, not the code path. Once a sort clears workerThreshold (50,000 rows by default) and the column’s comparator can travel to a worker, the comparison itself moves off the main thread so the tab keeps responding to scroll and input while it runs; filtering and grouping are not part of that handoff and always run inline, because each depends on the stage before it and a round trip would cost more than it saved. Because only the row order changes and not the row objects, memory stays flat rather than doubling with every sort. The result is a number on the page rather than an assurance: how long a click actually takes, on the machine reading it, at a scale most grids never show.

How fast can a JavaScript data grid sort a million rows?

The figure varies by machine and column type, which is why this demo times the sort live on the page rather than quoting a fixed number. Lattice Grid sorts by reordering an index rather than the underlying rows, and at a million rows a sortable column typically clears the threshold to run that comparison on a worker thread rather than the one painting the page, so the time shown reflects the comparison cost for the visible column at the row count loaded, not a network round trip.

Does sorting a JavaScript data grid block the main thread?

Below workerThreshold (50,000 rows by default), a sort runs inline because a worker handoff would cost more than the comparison it is meant to speed up. Past that threshold, a sort whose column comparator can travel to a worker moves there automatically, leaving the main thread free during the comparison. Filtering and grouping always run on the main thread, because each stage’s result feeds directly into the next.