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.

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≤ 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: false,
};

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 pre-computed sort key:

{
field: 'name',
sortable: true,
comparator: (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;
}}
  1. Open Chrome DevTools → Performance tab
  2. Record while scrolling or sorting
  3. Look for long tasks (> 50ms) in the flame chart
  4. Check for layout thrashing (forced reflows)
MetricTargetHow to Measure
Scroll FPS60fpsPerformance tab → Frames
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://raw.githubusercontent.com/OysteinAmundsen/toolbox/main/llms-full.txt