Skip to content

Commit

Permalink
[DataGrid] Scroll performance improvements (#9037)
Browse files Browse the repository at this point in the history
Signed-off-by: Rom Grk <romgrk@users.noreply.github.com>
Co-authored-by: Andrew Cherniavskii <andrew.cherniavskii@gmail.com>
Co-authored-by: Matheus Wichman <matheushw@outlook.com>
  • Loading branch information
3 people authored Jun 14, 2023
1 parent e957949 commit 8380eae
Show file tree
Hide file tree
Showing 40 changed files with 958 additions and 411 deletions.
10 changes: 1 addition & 9 deletions docs/data/data-grid/overview/DataGridProDemo.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import { DataGridPro, GridRow, GridColumnHeaders } from '@mui/x-data-grid-pro';
import { DataGridPro } from '@mui/x-data-grid-pro';
import { useDemoData } from '@mui/x-data-grid-generator';

const MemoizedRow = React.memo(GridRow);

const MemoizedColumnHeaders = React.memo(GridColumnHeaders);

export default function DataGridProDemo() {
const { data } = useDemoData({
dataSet: 'Commodity',
Expand All @@ -22,10 +18,6 @@ export default function DataGridProDemo() {
rowHeight={38}
checkboxSelection
disableRowSelectionOnClick
components={{
Row: MemoizedRow,
ColumnHeaders: MemoizedColumnHeaders,
}}
/>
</Box>
);
Expand Down
10 changes: 1 addition & 9 deletions docs/data/data-grid/overview/DataGridProDemo.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import { DataGridPro, GridRow, GridColumnHeaders } from '@mui/x-data-grid-pro';
import { DataGridPro } from '@mui/x-data-grid-pro';
import { useDemoData } from '@mui/x-data-grid-generator';

const MemoizedRow = React.memo(GridRow);

const MemoizedColumnHeaders = React.memo(GridColumnHeaders);

export default function DataGridProDemo() {
const { data } = useDemoData({
dataSet: 'Commodity',
Expand All @@ -22,10 +18,6 @@ export default function DataGridProDemo() {
rowHeight={38}
checkboxSelection
disableRowSelectionOnClick
components={{
Row: MemoizedRow,
ColumnHeaders: MemoizedColumnHeaders,
}}
/>
</Box>
);
Expand Down
4 changes: 0 additions & 4 deletions docs/data/data-grid/overview/DataGridProDemo.tsx.preview
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,4 @@
rowHeight={38}
checkboxSelection
disableRowSelectionOnClick
components={{
Row: MemoizedRow,
ColumnHeaders: MemoizedColumnHeaders,
}}
/>
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import { unstable_useForkRef as useForkRef } from '@mui/utils';
import { DataGridPro, GridRow, GridColumnHeaders } from '@mui/x-data-grid-pro';
import { DataGridPro, GridCell } from '@mui/x-data-grid-pro';
import { useDemoData } from '@mui/x-data-grid-generator';

const TraceUpdates = React.forwardRef((props, ref) => {
Expand All @@ -26,18 +26,15 @@ const TraceUpdates = React.forwardRef((props, ref) => {
return <Component ref={handleRef} {...other} />;
});

const RowWithTracer = React.forwardRef((props, ref) => {
return <TraceUpdates ref={ref} Component={GridRow} {...props} />;
const CellWithTracer = React.forwardRef((props, ref) => {
return <TraceUpdates ref={ref} Component={GridCell} {...props} />;
});

const ColumnHeadersWithTracer = React.forwardRef((props, ref) => {
return <TraceUpdates ref={ref} Component={GridColumnHeaders} {...props} />;
});

const MemoizedRow = React.memo(RowWithTracer);
const MemoizedColumnHeaders = React.memo(ColumnHeadersWithTracer);
const slots = {
cell: CellWithTracer,
};

export default function GridWithReactMemo() {
export default function GridVisualization() {
const { data } = useDemoData({
dataSet: 'Commodity',
rowLength: 100,
Expand All @@ -57,7 +54,7 @@ export default function GridWithReactMemo() {
}),
},
'&&& .updating': {
backgroundColor: 'rgb(92 199 68 / 25%)',
backgroundColor: 'rgb(92 199 68 / 20%)',
outline: '1px solid rgb(92 199 68 / 35%)',
outlineOffset: '-1px',
transition: 'none',
Expand All @@ -69,10 +66,7 @@ export default function GridWithReactMemo() {
rowHeight={38}
checkboxSelection
disableRowSelectionOnClick
slots={{
row: MemoizedRow,
columnHeaders: MemoizedColumnHeaders,
}}
slots={slots}
/>
</Box>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import { unstable_useForkRef as useForkRef } from '@mui/utils';
import { DataGridPro, GridRow, GridColumnHeaders } from '@mui/x-data-grid-pro';
import { DataGridPro, GridCell } from '@mui/x-data-grid-pro';
import { useDemoData } from '@mui/x-data-grid-generator';

const TraceUpdates = React.forwardRef<any, any>((props, ref) => {
Expand All @@ -26,18 +26,15 @@ const TraceUpdates = React.forwardRef<any, any>((props, ref) => {
return <Component ref={handleRef} {...other} />;
});

const RowWithTracer = React.forwardRef((props, ref) => {
return <TraceUpdates ref={ref} Component={GridRow} {...props} />;
const CellWithTracer = React.forwardRef((props, ref) => {
return <TraceUpdates ref={ref} Component={GridCell} {...props} />;
});

const ColumnHeadersWithTracer = React.forwardRef((props, ref) => {
return <TraceUpdates ref={ref} Component={GridColumnHeaders} {...props} />;
});

const MemoizedRow = React.memo(RowWithTracer);
const MemoizedColumnHeaders = React.memo(ColumnHeadersWithTracer);
const slots = {
cell: CellWithTracer,
};

export default function GridWithReactMemo() {
export default function GridVisualization() {
const { data } = useDemoData({
dataSet: 'Commodity',
rowLength: 100,
Expand All @@ -57,7 +54,7 @@ export default function GridWithReactMemo() {
}),
},
'&&& .updating': {
backgroundColor: 'rgb(92 199 68 / 25%)',
backgroundColor: 'rgb(92 199 68 / 20%)',
outline: '1px solid rgb(92 199 68 / 35%)',
outlineOffset: '-1px',
transition: 'none',
Expand All @@ -69,10 +66,7 @@ export default function GridWithReactMemo() {
rowHeight={38}
checkboxSelection
disableRowSelectionOnClick
slots={{
row: MemoizedRow,
columnHeaders: MemoizedColumnHeaders,
}}
slots={slots}
/>
</Box>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,5 @@
rowHeight={38}
checkboxSelection
disableRowSelectionOnClick
slots={{
row: MemoizedRow,
columnHeaders: MemoizedColumnHeaders,
}}
slots={slots}
/>
68 changes: 38 additions & 30 deletions docs/data/data-grid/performance/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,51 @@

<p class="description">Improve the performance of the DataGrid using the recommendations from this guide.</p>

## Memoize inner components with `React.memo`
## Extract static objects and memoize root props

The `DataGrid` component is composed of a central state object where all data is stored.
When an API method is called, a prop changes, or the user interacts with the UI (e.g. filtering a column), this state object is updated with the changes made.
To reflect the changes in the interface, the component must re-render.
Since the state behaves like `React.useState`, the `DataGrid` component will re-render its children, including column headers, rows, and cells.
With smaller datasets, this is not a problem for concern, but it can become a bottleneck if the number of rows increases, especially if many columns render [custom content](/x/react-data-grid/column-definition/#rendering-cells).
One way to overcome this issue is using `React.memo` to only re-render the child components when their props have changed.
To start using memoization, import the inner components, then pass their memoized version to the respective slots, as follow:
The `DataGrid` component uses `React.memo` to optimize its performance, which means itself and its subcomponents only
re-render when their props change. But it's very easy to cause unnecessary re-renders if the root props of your
`DataGrid` aren't memoized. Take the example below, the `slots` and `initialState` objects are re-created on every
render, which means the `DataGrid` itself has no choice but to re-render as well.

```tsx
import {
GridRow,
GridColumnHeaders,
DataGrid, // or DataGridPro, DataGridPremium
} from '@mui/x-data-grid';

const MemoizedRow = React.memo(GridRow);
const MemoizedColumnHeaders = React.memo(GridColumnHeaders);

<DataGrid
slots={{
row: MemoizedRow,
columnHeaders: MemoizedColumnHeaders,
}}
/>;
function Component({ rows }) {
return (
<DataGrid
rows={rows}
slots={{
row: CustomRow,
}}
cellModesModel={{ [rows[0].id]: { name: { mode: GridCellModes.Edit } } }}
/>
);
}
```

The following demo show this trick in action.
It also contains additional logic to highlight the components when they re-render.
An easy way to prevent re-renders is to extract any object that can be a static object, and to memoize any object that
depends on another object. This applies to any prop that is an object or a function.

{{"demo": "GridWithReactMemo.js", "bg": "inline", "defaultCodeOpen": false}}
```tsx
const slots = {
row: CustomRow,
};

function Component({ rows }) {
const cellModesModel = React.useMemo(
() => ({ [rows[0].id]: { name: { mode: GridCellModes.Edit } } }),
[rows],
);

return <DataGrid rows={rows} slots={slots} cellModesModel={cellModesModel} />;
}
```

## Visualization

The DataGrid memoizes some of its subcomponents to avoid re-rendering more than needed. Below is a visualization that
shows you which cells re-render in reaction to your interaction with the grid.

:::warning
We do not ship the components above already wrapped with `React.memo` because if you have rows whose cells display custom content not derived from the received props, e.g. selectors, these cells may display outdated information.
If you define a column with a custom cell renderer where content comes from a [selector](/x/react-data-grid/state/#catalog-of-selectors) that changes more often than the props passed to `GridRow`, the row component should not be memoized.
:::
{{"demo": "GridVisualization.js", "bg": "inline", "defaultCodeOpen": false}}

## API

Expand Down
3 changes: 2 additions & 1 deletion docs/data/data-grid/state/state.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ const paginationModel = gridPaginationModelSelector(

### With useGridSelector

If you only need to access the state value in the render of your components, use the `useGridSelector` hook:
If you only need to access the state value in the render of your components, use the `useGridSelector` hook.
This hook ensures there is a reactive binding such that when the state changes, the component in which this hook is used is re-rendered.

```tsx
const paginationModel = useGridSelector(apiRef, gridPaginationModelSelector);
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"l10n": "babel-node -x .ts ./scripts/l10n.ts",
"jsonlint": "node ./scripts/jsonlint.mjs",
"eslint": "eslint . --cache --report-unused-disable-directives --ext .js,.ts,.tsx --max-warnings 0",
"eslint:fix": "yarn eslint --fix",
"eslint:ci": "eslint . --report-unused-disable-directives --ext .js,.ts,.tsx --max-warnings 0",
"markdownlint": "markdownlint-cli2 \"**/*.md\"",
"postinstall": "patch-package",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ describe('<DataGridPro /> - Row Editing', () => {
apiRef.current.setEditCellValue({ id: 0, field: 'currencyPair', value: ' usdgbp ' }),
);
await act(() => apiRef.current.setEditCellValue({ id: 0, field: 'price1M', value: 100 }));
expect(renderEditCell1.lastCall.args[0].row).to.deep.equal({
expect(renderEditCell2.lastCall.args[0].row).to.deep.equal({
...defaultData.rows[0],
currencyPair: 'usdgbp',
price1M: 100,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,15 +300,12 @@ describe('<DataGridPro /> - Rows', () => {
);
}

// For some reason the number of renders in test env is 2x the number of renders in the browser
const renrederMultiplier = 2;

render(<Test />);
const initialRendersCount = 2;
expect(renderCellSpy.callCount).to.equal(initialRendersCount * renrederMultiplier);
expect(renderCellSpy.callCount).to.equal(initialRendersCount);

act(() => apiRef.current.updateRows([{ id: 1, name: 'John' }]));
expect(renderCellSpy.callCount).to.equal((initialRendersCount + 2) * renrederMultiplier);
expect(renderCellSpy.callCount).to.equal(initialRendersCount + 2);
});
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import { fastMemo } from '../utils/fastMemo';
import {
useGridColumnHeaders,
UseGridColumnHeadersProps,
Expand Down Expand Up @@ -113,4 +114,6 @@ GridColumnHeaders.propTypes = {
visibleColumns: PropTypes.arrayOf(PropTypes.object).isRequired,
} as any;

export { GridColumnHeaders };
const MemoizedGridColumnHeaders = fastMemo(GridColumnHeaders);

export { MemoizedGridColumnHeaders as GridColumnHeaders };
Loading

0 comments on commit 8380eae

Please sign in to comment.