Skip to content

Performance

@toolbox-web/grid is engineered for high performance out of the box. This guide helps you understand the performance characteristics and tune the grid for your specific workload.

A short explanation lives here so you don’t have to dig — for the full architectural narrative (render scheduler, virtualization, DOM recycling, template cloning, faux scrollbar) see the Architecture deep-dive.

TechniqueWhat it buys you
Centralized render schedulerAll updates coalesce into a single requestAnimationFrame — no layout thrashing, no double work
Row + column virtualizationRendering 100k rows costs the same as rendering 50
DOM recycling with epoch invalidationRow elements are reused via a pool — minimal GC, no element churn
Template cloning (cloneNode(true))3-4× faster than createElement + attribute assignment
Event delegationOne listener per event type on the container — scales to any dataset
Faux scrollbarScroll container is separate from the rendered content — zero reflow on scroll
Phase-priority executionHighest-priority phase wins per frame — full re-renders absorb sub-renders

The grid only renders rows visible in the viewport plus a configurable overscan buffer. This means rendering 100,000 rows is as fast as rendering 50 rows.

grid.gridConfig = {
columns: [...],
rowHeight: 28, // Fixed row height in pixels (default: auto-measured)
};
  • Default row height: Auto-measured from first row (respects --tbw-row-height CSS variable)
  • Variable row heights: Supported via rowHeight: (row, index) => number | undefined

For grids with many columns (50+), enable column virtualization:

import '@toolbox-web/grid/features/column-virtualization';
grid.gridConfig = {
columns: manyColumns,
features: { columnVirtualization: true },
};

All rendering is batched through a centralized RenderScheduler that coalesces multiple update requests into a single requestAnimationFrame callback.

Render phases (lowest → highest priority):

PhasePriorityWhat It Does
STYLE1CSS custom property updates
VIRTUALIZATION2Scroll position + visible window recalc
HEADER3Header row re-render
ROWS4Data row re-render
COLUMNS5Column structure rebuild
FULL6Complete re-render

When multiple phases are requested in the same frame, only the highest-priority phase executes (it inherently covers lower phases).

The most important optimization: only import the features you use.

// ✅ Optimal: ~45 KB core + only features you need
import '@toolbox-web/grid';
import '@toolbox-web/grid/features/selection';
import '@toolbox-web/grid/features/editing';
// ❌ Heavy: imports ALL plugins even if you use two
import '@toolbox-web/grid/all';
BundleRawGzipped
Core (index.js)≤ 170 KB≤ 50 KB (soft warning at 45 KB)
Individual plugin2–15 KB1–5 KB
All plugins (all.js)~300 KB~80 KB

Variable row heights (rowHeight: (row) => number) require measuring each row, which is slower than fixed heights. For datasets over 10,000 rows, prefer fixed heights:

grid.gridConfig = {
rowHeight: 32,
};

Row animations add visual polish but cost CPU cycles. Disable them for very large datasets:

grid.gridConfig = {
animation: { mode: 'off' },
};

By default mode is 'reduced-motion' — animations are already disabled when the user has prefers-reduced-motion: reduce set.

When updating many rows, assign the entire array at once rather than using insertRow() / removeRow() in a loop:

// ✅ Single assignment — triggers one sort/filter/render cycle
grid.rows = updatedData;
// ❌ Loop — triggers N animations and N re-renders
for (const newRow of newRows) {
await grid.insertRow(0, newRow);
}

When using filtering with a large dataset, increase the debounce to reduce re-filtering:

import '@toolbox-web/grid/features/filtering';
grid.gridConfig = {
features: { filtering: { debounceMs: 300 } }, // Default: 200
};

For fields with expensive comparisons, provide a custom sortComparator:

{
field: 'name',
sortable: true,
sortComparator: (a: string, b: string) =>
a.localeCompare(b, undefined, { sensitivity: 'base' }),
}

Let the grid manage its DOM. Direct manipulation can conflict with the render scheduler and virtualization:

// ❌ Don't do this — bypasses the grid's render cycle
document.querySelector('.data-grid-row')!.style.background = 'red';
// ✅ Use rowClass/cellClass or a renderer instead
grid.gridConfig = {
rowClass: (row) => row.status === 'error' ? 'row-error' : '',
};

Use registerStyles() for Dynamic Runtime CSS

Section titled “Use registerStyles() for Dynamic Runtime CSS”

Standard CSS (stylesheets, <style> in <head>) works fine for static styles. For styles you need to inject or toggle from JavaScript at runtime, use registerStyles() instead of appending <style> elements inside the grid (which get removed by replaceChildren()):

grid.registerStyles('my-highlights', `
.highlight-row { background: yellow; }
`);
// Clean up when done
grid.unregisterStyles('my-highlights');

Formatters return a plain string — significantly faster than creating DOM elements via a renderer. Use a renderer only when you need interactive elements or custom HTML structure:

// ✅ Fast — formatter returns a string (no DOM creation)
{ field: 'salary', format: (v) => `$${v.toFixed(2)}` }
// ⚠️ Slower — renderer creates DOM elements (use only when needed)
{ field: 'status', renderer: (ctx) => {
const badge = document.createElement('span');
badge.className = `badge-${ctx.value}`;
badge.textContent = String(ctx.value);
return badge;
}}

Use this table to set expectations and pick which knobs matter for your dataset. All numbers assume the default plugin set on a 2023-class laptop (M-series Mac / Ryzen 7).

Rows × ColsWhat to expectWhat you’ll tune
≤ 1k × 20Instant — no tuning neededNothing
10k × 20Smooth 60fps scroll / sort < 50msrowHeight: number (fixed) over variable; prefer format over renderer
100k × 20Smooth scroll; sort 200-500ms (single-threaded JS)Same as above + getRowId for stable identity; consider Server-Side for queries
1M+ rowsDon’t load into memory — paginate or use Server-SideServer-Side plugin with infinite scroll
× 50+ columnsHorizontal scroll may stutter without column virtualizationEnable ColumnVirtualizationPlugin

The “is the grid slow?” question is almost always one of: (a) the grid is doing too much work (too many columns rendered, expensive renderers, layout thrashing from outside the grid); or (b) your data pipeline is slow (re-allocating row arrays, re-deriving columns on every render, heavy formatters). The DevTools Performance tab can tell the two apart in 60 seconds.

  1. Record a representative interaction. Open Chrome DevTools → Performance → click ⏺ Record → do the slow thing (scroll, sort, filter, edit) for 2-3 seconds → stop.

  2. Find the long tasks. Anything > 50ms shows up as a red triangle. Click into the flame graph.

  3. Read the stack:

    • renderScheduler.flush dominates → grid-internal work. Look one level deeper:
      • renderRow / updateRowDom heavy → your renderer or cellClass is slow. Switch to format if you only return text.
      • measureRow heavy → variable row heights with expensive content. Set a fixed rowHeight or memoize.
      • applyColumns heavy → too many visible columns. Enable ColumnVirtualizationPlugin.
    • Framework code (React reconciler / Angular CD / Vue render) dominates → your wrapper is re-rendering the grid on unrelated state changes. Pass rows / gridConfig as a stable reference (useMemo, computed, signal).
    • Recalculate Style / Layout dominate, outside the grid → something on the page is reacting to the grid’s resize. Usually a parent flexbox or a ResizeObserver you wrote.
  4. Mark your own boundaries to make the trace readable:

    performance.mark('app:load-rows:start');
    grid.rows = bigArray;
    await new Promise(r => requestAnimationFrame(r));
    performance.mark('app:load-rows:end');
    performance.measure('app:load-rows', 'app:load-rows:start', 'app:load-rows:end');

    performance.measure() entries show up as labeled bars at the top of the flame chart.

  5. Compare against the comparison page — if your numbers are very different from ours on a similar dataset, the cause is in your app code, not the grid.

MetricTargetHow to measure
Scroll FPS60fpsDevTools → Performance → Frames row
Sort time (10K rows)< 50msconsole.time() around grid.rows = sorted
Filter time (10K rows)< 30msMeasure in filter-change event handler
Initial render< 100msPerformance tab → First Paint
Memory (100K rows)< 100 MBMemory tab → Heap snapshot

Try the interactive stress test below — adjust row and column counts to see how the grid performs with large datasets:

Ready

Click Run Full Benchmark to test all grid operations.

Tests: Initial render, scroll performance, sort, filter, data update, selection, memory usage

AI assistants: For complete API documentation, implementation guides, and code examples for this library, see https://toolboxjs.com/llms-full.txt