The DataGrid uses row and column virtualization to efficiently render large datasets by only rendering the rows and columns that are currently visible in the viewport, plus a small buffer (overscan) for smooth scrolling.
Virtualization is a technique that dramatically improves performance when working with large datasets. Instead of rendering all rows in the DOM (which would be slow and memory-intensive), the grid only renders:
- Visible rows - Rows currently in the viewport
- Overscan rows - A buffer of rows above and below the viewport for smooth scrolling
- Pinned rows - Rows that are always visible (top/bottom)
The grid calculates which rows are visible based on:
- Current scroll position
- Viewport height
- Row height (fixed or variable)
- Overscan buffer (default: 5 rows)
// Example: 1000 rows, but only ~15 rendered at a time
<DataGrid
rows={thousandRows}
columns={columns}
style={{ height: 600 }}
/>Performance characteristics:
- ✅ Renders ~15-25 rows regardless of total dataset size
- ✅ Constant memory usage
- ✅ Smooth 60fps scrolling
- ✅ Supports millions of rows
Currently, the DataGrid renders all columns. Column virtualization (rendering only visible columns) is planned for future releases.
The grid supports both fixed and variable row heights:
// Fixed row height (default: 52px)
<DataGrid
rows={rows}
columns={columns}
rowHeight={40}
/>The density prop overrides rowHeight with preset values:
<DataGrid
rows={rows}
columns={columns}
density="compact" // 32px rows
// density="standard" // uses rowHeight (default 52px)
// density="comfortable" // 72px rows
/>Disable virtualization for small datasets:
<DataGrid
rows={rows}
columns={columns}
autoHeight
/>autoHeight disables row virtualization and renders all rows. Only use for small datasets (< 100 rows).
The overscan buffer determines how many extra rows are rendered outside the viewport. The grid adapts this buffer automatically based on scroll velocity so fast scrolls never produce blank flashes.
Adaptive algorithm (useGridScrollSync):
- Scroll velocity (px/ms) is measured on every scroll event
- The overscan tier is chosen from the table below and applied immediately
- 200 ms after scrolling stops the buffer decays back to
overscanRowCount
| Velocity (px/ms) | Overscan rows |
|---|---|
| < 0.5 | 3 |
| 0.5 – 3 | 5 |
| 3 – 15 | 12 |
| 15 – 40 | 20 |
| > 40 | 30 |
Configuring the floor:
// Never render fewer than 5 rows outside the viewport
<DataGrid
rows={rows}
columns={columns}
overscanRowCount={5}
/>overscanRowCount (default 3) sets the minimum — the adaptive algorithm always produces a value ≥ this floor.
Trade-offs:
- Smaller floor (1-3): Less idle memory, relies on adaptive headroom for fast scrolls
- Larger floor (5-8): Pre-renders more rows at rest, trades memory for smoother slow scrolls
For optimal scroll performance:
- Use fixed row heights when possible
- Avoid complex cell renderers - Keep
renderCellfunctions lightweight - Memoize custom components - Use
React.memofor custom cell components - Minimize re-renders - Use stable column definitions
// ✅ Good: Memoized columns
const columns = useMemo(() => [
{ field: 'id', headerName: 'ID', width: 70 },
{ field: 'name', headerName: 'Name', width: 200 }
], []);
// ❌ Bad: New column array on every render
const columns = [
{ field: 'id', headerName: 'ID', width: 70 },
{ field: 'name', headerName: 'Name', width: 200 }
];When using detail panels, row heights become variable. The grid automatically:
- Calculates cumulative heights for each row
- Adjusts scroll calculations for expanded panels
- Maintains smooth scrolling
<DataGrid
rows={rows}
columns={columns}
getDetailPanelContent={({ row }) => <DetailPanel row={row} />}
getDetailPanelHeight={() => 200}
/>Row grouping and tree data work seamlessly with virtualization:
// Tree data with virtualization
<DataGrid
rows={rows}
columns={columns}
treeData
getTreeDataPath={(row) => row.path}
defaultGroupingExpansionDepth={-1} // Expand all
/>Note: When groups are collapsed, only visible rows are rendered. Expanding a group dynamically adds rows to the render list.
To see which rows are being rendered:
// Add this to your column definition
{
field: 'debug',
headerName: 'Debug',
renderCell: (params) => {
console.log('Rendering row:', params.row.id);
return params.row.id;
}
}You should see console logs only for visible rows + overscan buffer.
- Column virtualization - Not yet implemented (all columns are rendered)
- Dynamic row heights - Requires full dataset scan for accurate scroll calculations
- Horizontal scrolling - May show brief flicker with very wide grids
- Column virtualization for grids with 50+ columns
- Adaptive overscan based on scroll velocity (
overscanRowCountprop, v2.0.4) - Virtual scrollbar for extremely large datasets (millions of rows)
- Intersection Observer API for better scroll detection
| Feature | OpenGridX | MUI X DataGrid |
|---|---|---|
| Row virtualization | ✅ Yes | ✅ Yes |
| Column virtualization | ❌ Planned | ✅ Yes (Pro) |
| Variable row heights | ✅ Yes | ✅ Yes |
| Overscan buffer | 3–30 rows (adaptive, velocity-based) | 3-8 rows (adaptive) |
| Max recommended rows | 100,000+ | 100,000+ |