Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DataGrid] Make possible to memoize rows and cells #7846

Merged
merged 17 commits into from
Feb 27, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion docs/data/data-grid/overview/DataGridProDemo.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import { DataGridPro } from '@mui/x-data-grid-pro';
import {
DataGridPro,
GridRow,
GridCell,
DataGridProColumnHeaders,
} from '@mui/x-data-grid-pro';
import { useDemoData } from '@mui/x-data-grid-generator';

const MemoizedRow = React.memo(GridRow);

const MemoizedCell = React.memo(GridCell);
m4theushw marked this conversation as resolved.
Show resolved Hide resolved

const MemoizedColumnHeaders = React.memo(DataGridProColumnHeaders);

export default function DataGridProDemo() {
const { data } = useDemoData({
dataSet: 'Commodity',
Expand All @@ -18,6 +29,11 @@ export default function DataGridProDemo() {
rowHeight={38}
checkboxSelection
disableRowSelectionOnClick
components={{
Row: MemoizedRow,
Cell: MemoizedCell,
ColumnHeaders: MemoizedColumnHeaders,
}}
/>
</Box>
);
Expand Down
18 changes: 17 additions & 1 deletion docs/data/data-grid/overview/DataGridProDemo.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import { DataGridPro } from '@mui/x-data-grid-pro';
import {
DataGridPro,
GridRow,
GridCell,
DataGridProColumnHeaders,
} from '@mui/x-data-grid-pro';
import { useDemoData } from '@mui/x-data-grid-generator';

const MemoizedRow = React.memo(GridRow);

const MemoizedCell = React.memo(GridCell);

const MemoizedColumnHeaders = React.memo(DataGridProColumnHeaders);

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

const TraceUpdates = React.forwardRef((props, ref) => {
const { Component, ...other } = props;
const rootRef = React.useRef();
const handleRef = useForkRef(rootRef, ref);

React.useEffect(() => {
rootRef.current?.classList.add('updating');

const timer = setTimeout(() => {
rootRef.current?.classList.remove('updating');
}, 500);

return () => clearTimeout(timer);
});

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={DataGridProColumnHeaders} {...props} />;
});

const MemoizedRow = React.memo(RowWithTracer);
const MemoizedCell = React.memo(CellWithTracer);
const MemoizedColumnHeaders = React.memo(ColumnHeadersWithTracer);

export default function GridWithReactMemo() {
const { data } = useDemoData({
dataSet: 'Commodity',
rowLength: 100,
editable: true,
maxColumns: 15,
});

return (
<Box
sx={{
height: 400,
width: '100%',
'& .updating': {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
'& .updating': {
'&& .updating': {

To override the row hover background color, otherwise it might look like the row wasn't rerendered

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it's still not enough - &&& is needed to actually override the hover color

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't understand. The row doesn't appear to be rendering again on hover.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I managed to reproduce the bug, it's tricky to see it.

background: '#b2dfdb',
cherniavskii marked this conversation as resolved.
Show resolved Hide resolved
transition: (theme) =>
theme.transitions.create('background', {
duration: theme.transitions.duration.standard,
}),
},
}}
>
<DataGridPro
{...data}
rowHeight={38}
checkboxSelection
disableRowSelectionOnClick
components={{
Row: MemoizedRow,
Cell: MemoizedCell,
ColumnHeaders: MemoizedColumnHeaders,
}}
/>
</Box>
);
}
81 changes: 81 additions & 0 deletions docs/data/data-grid/performance/GridWithReactMemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import { unstable_useForkRef as useForkRef } from '@mui/utils';
import {
DataGridPro,
GridRow,
GridCell,
DataGridProColumnHeaders,
} from '@mui/x-data-grid-pro';
import { useDemoData } from '@mui/x-data-grid-generator';

const TraceUpdates = React.forwardRef<any, any>((props, ref) => {
const { Component, ...other } = props;
const rootRef = React.useRef<HTMLElement>();
const handleRef = useForkRef(rootRef, ref);

React.useEffect(() => {
rootRef.current?.classList.add('updating');

const timer = setTimeout(() => {
rootRef.current?.classList.remove('updating');
}, 500);

return () => clearTimeout(timer);
});

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={DataGridProColumnHeaders} {...props} />;
});

const MemoizedRow = React.memo(RowWithTracer);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it expected that the whole row is rerendered on cell focus change?

Screen.Recording.2023-02-20.at.19.52.49.mov

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, because which cell is focused comes from the focusedCell prop. If I remove this prop and pass it to the cell, from inside the row, then the row won't re-render because no prop has changed, even though the props passed to the cell will have changed.

const MemoizedCell = React.memo(CellWithTracer);
const MemoizedColumnHeaders = React.memo(ColumnHeadersWithTracer);

export default function GridWithReactMemo() {
const { data } = useDemoData({
dataSet: 'Commodity',
rowLength: 100,
editable: true,
maxColumns: 15,
});

return (
<Box
sx={{
height: 400,
width: '100%',
'& .updating': {
background: '#b2dfdb',
transition: (theme) =>
theme.transitions.create('background', {
duration: theme.transitions.duration.standard,
}),
},
}}
>
<DataGridPro
{...data}
rowHeight={38}
checkboxSelection
disableRowSelectionOnClick
components={{
Row: MemoizedRow,
Cell: MemoizedCell,
ColumnHeaders: MemoizedColumnHeaders,
}}
/>
</Box>
);
}
11 changes: 11 additions & 0 deletions docs/data/data-grid/performance/GridWithReactMemo.tsx.preview
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<DataGridPro
{...data}
rowHeight={38}
checkboxSelection
disableRowSelectionOnClick
components={{
Row: MemoizedRow,
Cell: MemoizedCell,
ColumnHeaders: MemoizedColumnHeaders,
}}
/>
72 changes: 72 additions & 0 deletions docs/data/data-grid/performance/performance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Data Grid - Performance

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

## Memoize inner components with `React.memo`

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`, it means that the `DataGrid` component will re-render as well as its children, which includes column headers, rows and cells.
m4theushw marked this conversation as resolved.
Show resolved Hide resolved
With smaller datasets, this is not a problem for concern, but it can become a bottleneck if the number of rows gets increased and, specially, if many columns render [custom content](/x/react-data-grid/column-definition/#rendering-cells).
m4theushw marked this conversation as resolved.
Show resolved Hide resolved
One way to overcome this issue is using `React.memo` to only re-render the children components when their props have changed.
m4theushw marked this conversation as resolved.
Show resolved Hide resolved
To start using memoization, import the inner components, then pass their memoized version to the respective slots, as follow:

```tsx
import {
GridRow,
GridCell,
DataGrid, // or DataGridPro, DataGridPremium
DataGridColumnHeaders, // or DataGridProColumnHeaders, DataGridPremiumColumnHeaders
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side note: This seems inconsistent with the other components' names that do not change regardless of the package used (one exception is DataGrid/DataGridPro/DataGridPremium).
Should we rename it to GridColumnHeaders for all packages?

} from '@mui/x-data-grid';

const MemoizedRow = React.memo(GridRow);
const MemoizedCell = React.memo(GridCell);
const MemoizedColumnHeaders = React.memo(DataGridColumnHeaders);

<DataGrid
components={{
Row: MemoizedRow,
Cell: MemoizedCell,
ColumnHeaders: MemoizedColumnHeaders,
}}
/>;
```

The following demo show this trick in action.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't add a "before" demo because the comparison would be too ridiculous but here's https://codesandbox.io/s/crimson-tdd-fb0vec?file=/demo.tsx. Basically, in any click everything re-renders.

It also contains additional logic to highlight the components when they re-render.

{{"demo": "GridWithReactMemo.js", "bg": "inline", "defaultCodeOpen": false}}

:::warning
We do not ship the components above already wrapped with `React.memo` because if you have cells that display custom content whose source is not the received props, these cells may display outdated information.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we memoize the Cell slot though?
renderCell is called in GridRow component and the returned value is passed to the GridCell.
So if there's renderCell in the column - the cell will always rerender - see this demo: https://codesandbox.io/s/dreamy-roentgen-px945x

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, I added React.memo to the cell component by default now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about this more, wouldn't it make more sense to call renderCell inside the GridCell component?
This should allow memoizing the GridRow by default (as opposed to GridCell), and then the GridCell can be memoized by users. (Maybe then we could go a step further and add a check in React.memo to memoize GridCell by default if colDef.renderCell is defined?).
If it's too time-consuming, we can work on this later - this PR is already a great improvement!

What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we add this check to React.memo we lose the default shallow comparison function. I don't know what's the best approach here. I propose to leave this as it is and see the implications.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way, is there a way to reduce rerenders of the cells that have renderCell? Would memoizing the component returned by renderCell work?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would memoizing the component returned by renderCell work?

I tested and it didn't work. To memoize cells with custom renderers, we should call renderCell and cache its content, then invalidate the cached value if some prop changes. If we use the row prop as cache key, we fall again into the problem of the cell renderer depending on a selector. It's better to leave this for the user to cache the value. We can do a follow-up explaining how to do this.

For instance, 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 then the props passed to `GridCell` and `GridRow`, the row and column should not be memoized.
You can choose whether to memoize or not a component by passing a 2nd argument to `React.memo`:

```tsx
function shallowCompare(prevProps, nextProps) {
const aKeys = Object.keys(prevProps);
const bKeys = Object.keys(nextProps);

if (aKeys.length !== bKeys.length) {
return false;
}

return aKeys.every(
(key) => nextProps.hasOwnProperty(key) && nextProps[key] === prevProps[key],
);
}

const MemoizedCell = React.memo(GridCell, (prevProps, nextProps) => {
// Prevent memoizing the cells from the "total" column
return nextProps.field !== 'total' && shallowCompare(prevProps, nextProps);
});
```

:::

## API

- [DataGrid](/x/api/data-grid/data-grid/)
- [DataGridPro](/x/api/data-grid/data-grid-pro/)
- [DataGridPremium](/x/api/data-grid/data-grid-premium/)
Original file line number Diff line number Diff line change
Expand Up @@ -390,13 +390,13 @@ Most of this breaking change is handled by `preset-safe` codemod but some furthe
| `.MuiDataGrid-withBorder` | `.MuiDataGrid-withBorderColor` | The class only sets `border-color` CSS property |
| `.MuiDataGrid-filterFormLinkOperatorInput` | `.MuiDataGrid-filterFormLogicOperatorInput` | |

<!--
### Virtualization

TBD
-->

### Removals from the public API

- The `getGridSingleSelectQuickFilterFn` function was removed.
You can copy the old function and pass it to the `getApplyQuickFilterFn` property of the `singleSelect` column definition.

### Misc

- The `cellFocus`, `cellTabIndex` and `editRowsState` props are not passed to the component used in the row slot.
You can use the new `focusedCell` and `tabbableCell` props instead.
For the editing state, use the API methods.
m4theushw marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions docs/data/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const pages: MuiPage[] = [
{ pathname: '/x/react-data-grid/scrolling' },
{ pathname: '/x/react-data-grid/virtualization' },
{ pathname: '/x/react-data-grid/accessibility' },
{ pathname: '/x/react-data-grid/performance' },
{
pathname: '/x/react-data-grid-group-pivot',
title: 'Group & Pivot',
Expand Down
1 change: 1 addition & 0 deletions docs/pages/x/api/data-grid/data-grid-premium.json
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@
"default": "GridColumnHeaderFilterIconButton",
"type": { "name": "elementType" }
},
"ColumnHeaders": { "default": "DataGridColumnHeaders", "type": { "name": "elementType" } },
"ColumnMenu": { "default": "GridColumnMenu", "type": { "name": "elementType" } },
"ColumnMenuAggregationIcon": {
"default": "GridFunctionsIcon",
Expand Down
1 change: 1 addition & 0 deletions docs/pages/x/api/data-grid/data-grid-pro.json
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@
"default": "GridColumnHeaderFilterIconButton",
"type": { "name": "elementType" }
},
"ColumnHeaders": { "default": "DataGridColumnHeaders", "type": { "name": "elementType" } },
"ColumnMenu": { "default": "GridColumnMenu", "type": { "name": "elementType" } },
"ColumnMenuClearIcon": { "default": "GridClearIcon", "type": { "name": "elementType" } },
"ColumnMenuFilterIcon": { "default": "GridFilterAltIcon", "type": { "name": "elementType" } },
Expand Down
1 change: 1 addition & 0 deletions docs/pages/x/api/data-grid/data-grid.json
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@
"default": "GridColumnHeaderFilterIconButton",
"type": { "name": "elementType" }
},
"ColumnHeaders": { "default": "DataGridColumnHeaders", "type": { "name": "elementType" } },
"ColumnMenu": { "default": "GridColumnMenu", "type": { "name": "elementType" } },
"ColumnMenuClearIcon": { "default": "GridClearIcon", "type": { "name": "elementType" } },
"ColumnMenuFilterIcon": { "default": "GridFilterAltIcon", "type": { "name": "elementType" } },
Expand Down
7 changes: 7 additions & 0 deletions docs/pages/x/react-data-grid/performance.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as React from 'react';
import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs';
import * as pageProps from 'docsx/data/data-grid/performance/performance.md?@mui/markdown';

export default function Page() {
return <MarkdownDocs {...pageProps} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,7 @@
"Cell": "Component rendered for each cell.",
"ColumnFilteredIcon": "Icon displayed on the column header menu to show that a filter has been applied to the column.",
"ColumnHeaderFilterIconButton": "Filter icon component rendered in each column header.",
"ColumnHeaders": "Component responsible for rendering the column headers.",
"ColumnMenu": "Column menu component rendered by clicking on the 3 dots &quot;kebab&quot; icon in column headers.",
"ColumnMenuAggregationIcon": "Icon displayed in column menu for aggregation",
"ColumnMenuClearIcon": "Icon displayed in column menu for clearing values",
Expand Down
Loading