Skip to main content

Performance

A grid that handles 100 rows fine but chokes at 50,000 isn't production-ready. OGrid ships with three complementary features to handle large datasets without framework-level heroics:

FeatureOpt-in?What it does
CSS ContainmentNo (automatic)Browser skips layout and paint for off-screen cells
Column VirtualizationYesRenders only visible columns — scales to hundreds of columns
Web Worker Sort/FilterYesSort and filter run in a background thread — UI stays responsive

For rendering only visible rows (usually the most impactful optimization for tall datasets), see Virtual Scrolling.


CSS Containment — you get this for free

Every grid cell already has contain: content applied — you don't configure anything. This tells the browser that what happens inside a cell doesn't affect anything outside it, so it can skip layout and paint work for cells not visible in the viewport.

/* Applied automatically to every body cell */
td.ogrid-cell {
contain: content; /* isolate layout + paint + style */
}

/* Pinned columns use position: sticky — containment relaxed to avoid conflicts */
td.ogrid-cell[data-pinned] {
contain: none;
}

Without row virtualization, OGrid also sets content-visibility: auto on rows, letting the browser skip off-screen row layout entirely:

/* Browser skips layout for rows not in the viewport */
tr[data-row]:not([data-virtual-scroll]) {
content-visibility: auto;
}

When row virtualization is active, spacer rows handle positioning instead — so content-visibility is not applied there. The data-virtual-scroll attribute on <table> controls this automatically.

Net result: even without opting into anything, a 500-row grid renders measurably faster than without these rules.


Web Worker Sort/Filter — keeps the UI responsive

Sorting 50,000 rows on the main thread can freeze the UI for a visible moment. With workerSort: true, the work happens in a background thread — the grid stays interactive while it processes.

<OGrid
columns={columns}
data={largeDataset} // 50,000 rows
getRowId={(r) => r.id}
workerSort={true}
/>

Full example

import { OGrid } from '@alaarab/ogrid-react-radix';
import type { IColumnDef } from '@alaarab/ogrid-react-radix';

interface Row {
id: number;
name: string;
revenue: number;
region: string;
}

const columns: IColumnDef<Row>[] = [
{ columnId: 'id', name: 'ID', type: 'numeric' },
{ columnId: 'name', name: 'Name', sortable: true },
{ columnId: 'revenue', name: 'Revenue', type: 'numeric', sortable: true },
{ columnId: 'region', name: 'Region', sortable: true },
];

function App() {
return (
<OGrid
columns={columns}
data={largeDataset} // e.g. 50,000 rows
getRowId={(r) => r.id}
workerSort={true}
statusBar
/>
);
}
Switching UI libraries

Same props across all React packages — just change the import:

  • Radix (lightweight, default): from '@alaarab/ogrid-react-radix'
  • Fluent UI (Microsoft 365 / SPFx): from '@alaarab/ogrid-react-fluent' - wrap in <FluentProvider>
  • Material UI (MUI v7): from '@alaarab/ogrid-react-material' - wrap in <ThemeProvider>

Not sure about your data size? Use 'auto'

workerSort: 'auto' uses the worker only when your dataset exceeds 5,000 rows. For smaller datasets it runs synchronously — no overhead, no complexity:

<OGrid workerSort="auto" ... />

This is the right default if you're not sure how large your data will get.

How it works under the hood

  1. Before sort/filter, the grid extracts a flat value matrix from your row data.
  2. The matrix is sent to an inline Blob Web Worker via postMessage.
  3. The worker sorts/filters and returns the resulting row indices.
  4. The grid re-renders using those indices — no row data is ever copied back, only the index array.

The worker is created once per grid instance and reused for all subsequent sort/filter operations.

When the worker falls back to sync

The worker can't run in every situation. It falls back silently in these cases:

SituationWhy
Column has a custom compare functionFunctions can't cross the worker boundary
people filter typeRequires rich object comparison
Worker API unavailable (old browser, sandboxed iframe)No Worker API to use

In all cases, the grid sorts and filters correctly — just on the main thread.

CSP gotcha

The worker uses an inline Blob URL. If your app has a Content-Security-Policy header, add blob: to worker-src:

Content-Security-Policy: worker-src 'self' blob:;

Without it, the browser blocks the worker and the grid falls back to synchronous processing. If you can't change the CSP, workerSort still works — it just runs on the main thread.


Putting it all together

Row virtualization, column virtualization, and worker sort/filter stack together cleanly. For a 100-column, 100,000-row dataset, enable all three:

import { OGrid } from '@alaarab/ogrid-react-radix';

function App() {
return (
<OGrid
columns={columns} // 100 columns
data={data} // 100,000 rows
getRowId={(r) => r.id}
virtualScroll={{
enabled: true, // renders ~30 rows instead of 100,000
rowHeight: 36,
overscan: 5,
columns: true, // renders only visible columns
columnOverscan: 2,
}}
workerSort={true} // sort/filter off the main thread
statusBar
/>
);
}
Switching UI libraries

Same props across all React packages — just change the import:

  • Radix (lightweight, default): from '@alaarab/ogrid-react-radix'
  • Fluent UI (Microsoft 365 / SPFx): from '@alaarab/ogrid-react-fluent' - wrap in <FluentProvider>
  • Material UI (MUI v7): from '@alaarab/ogrid-react-material' - wrap in <ThemeProvider>

Which features do I actually need?

Start here, add as needed:

DatasetRecommended setup
< 1,000 rows, < 20 columnsNothing — CSS containment handles it automatically.
1,000 – 10,000 rowsAdd row virtualization: virtualScroll: { enabled: true, rowHeight: 36 }.
> 10,000 rowsRow virtualization + workerSort: 'auto'.
> 30 columns with horizontal scrollAdd columns: true to the virtualScroll config.
Very large + very wideAll three: row virtualization, column virtualization, and workerSort: true.
Pro tip

If you're unsure whether you need column virtualization, open DevTools and look at how many <td> elements render in a single row. If it's more than you can count comfortably, add columns: true.


Props reference

workerSort

ValueWhen to use it
false (default)Small datasets, or when you have custom compare functions
trueLarge datasets where you want the worker always active
'auto'You don't know the data size ahead of time — use this as the safe default

Column virtualization fields (inside virtualScroll)

FieldTypeDefaultDescription
columnsbooleanfalseEnable column virtualization. Requires row virtualization to also be enabled.
columnOverscannumber2Extra columns rendered beyond the visible range on each side (prevents blank flash on fast scroll).

For the full virtualScroll API, see Virtual Scrolling — Props Reference.


Next steps

  • Virtual Scrolling — row virtualization, scroll-to-row, and the full virtualScroll API
  • Sorting — configure sortable columns and custom comparators
  • Filtering — filter types and server-side filtering
  • Server-Side Data — skip client-side processing entirely for the largest datasets
  • Column Pinning — pinned columns always render regardless of column virtualization