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

Conversation

m4theushw
Copy link
Member

@m4theushw m4theushw commented Feb 6, 2023

First step for #7807

Preview: https://deploy-preview-7846--material-ui-x.netlify.app/x/react-data-grid/performance/

Changelog

Breaking changes

  • 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 m4theushw added the component: data grid This is the name of the generic UI component, not the React module! label Feb 6, 2023
@mui-bot
Copy link

mui-bot commented Feb 6, 2023

Messages
📖 Netlify deploy preview: https://deploy-preview-7846--material-ui-x.netlify.app/

These are the results for the performance tests:

Test case Unit Min Max Median Mean σ
Filter 100k rows ms 663.6 996.9 738.7 823.88 142.119
Sort 100k rows ms 620.2 1,161.5 1,161.5 961.4 205.379
Select 100k rows ms 262.5 351.1 306.9 308.7 31.769
Deselect 100k rows ms 156.8 358.4 223 232.34 74.246

Generated by 🚫 dangerJS against 723a823

@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged label Feb 7, 2023
@github-actions
Copy link

github-actions bot commented Feb 7, 2023

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged label Feb 7, 2023
@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged label Feb 8, 2023
@github-actions
Copy link

github-actions bot commented Feb 8, 2023

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@cherniavskii cherniavskii added this to the v6-beta milestone Feb 10, 2023
@m4theushw m4theushw mentioned this pull request Feb 13, 2023
2 tasks
@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged label Feb 15, 2023
@@ -27,13 +28,15 @@ const DataGridProRaw = React.forwardRef(function DataGridPro<R extends GridValid
const privateApiRef = useDataGridProComponent(props.apiRef, props);
useLicenseVerifier('x-data-grid-pro', releaseInfo);

const pinnedColumns = useGridSelector(privateApiRef, gridPinnedColumnsSelector);
Copy link
Member Author

Choose a reason for hiding this comment

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

The pinned columns must be passed via props now otherwise, once the column headers are memoized, they won't reflect the changes.

Comment on lines -80 to -105
const visibleColumns = useGridSelector(apiRef, gridVisibleColumnDefinitionsSelector);
const columnPositions = useGridSelector(apiRef, gridColumnPositionsSelector);
const columnHeaderTabIndexState = useGridSelector(apiRef, gridTabIndexColumnHeaderSelector);
const cellTabIndexState = useGridSelector(apiRef, gridTabIndexCellSelector);
const columnGroupHeaderTabIndexState = useGridSelector(
apiRef,
unstable_gridTabIndexColumnGroupHeaderSelector,
);
const columnHeaderFocus = useGridSelector(apiRef, gridFocusColumnHeaderSelector);
const columnGroupHeaderFocus = useGridSelector(
apiRef,
unstable_gridFocusColumnGroupHeaderSelector,
);
const densityFactor = useGridSelector(apiRef, gridDensityFactorSelector);
const headerGroupingMaxDepth = useGridSelector(apiRef, gridColumnGroupsHeaderMaxDepthSelector);
const filterColumnLookup = useGridSelector(apiRef, gridFilterActiveItemsLookupSelector);
const sortColumnLookup = useGridSelector(apiRef, gridSortColumnLookupSelector);
const columnMenuState = useGridSelector(apiRef, gridColumnMenuSelector);
const columnVisibility = useGridSelector(apiRef, gridColumnVisibilityModelSelector);
const columnGroupsHeaderStructure = useGridSelector(
apiRef,
gridColumnGroupsHeaderStructureSelector,
);
Copy link
Member Author

Choose a reason for hiding this comment

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

Using selectors may cause problems if the component is wrapped in React.memo. The solution is to pass the selected value via props, which makes the component to re-render if the value changes.

Comment on lines 524 to 531
if (invalidatesCachedRenderedColumns) {
cachedRenderedColumns.current = visibleColumns.slice(firstColumnToRender, lastColumnToRender);
prevFirstColumnToRender.current = firstColumnToRender;
prevLastColumnToRender.current = lastColumnToRender;
prevVisibleColumns.current = visibleColumns;
}

const renderedColumns = cachedRenderedColumns.current;
Copy link
Member Author

Choose a reason for hiding this comment

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

The previous approach with a simple Array.slice was making the renderedColumns reference to change in every render. Here I created a small cache for it.

Comment on lines -551 to -555
style={{
...rowStyle,
...rootRowStyle,
}}
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 kept like that, the component will never be memoized because style is always changing. To support React.memo, the styles are computed once and saved to a cache.

/>;
```

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.

@m4theushw m4theushw marked this pull request as ready for review February 15, 2023 18:47
{{"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.

{{"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.

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.

@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged label Feb 17, 2023
@github-actions
Copy link

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged label Feb 20, 2023
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.

docs/data/data-grid/performance/GridWithReactMemo.js Outdated Show resolved Hide resolved
import {
GridRow,
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?

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.

docs/data/data-grid/overview/DataGridProDemo.js Outdated Show resolved Hide resolved
{{"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.

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?

packages/grid/x-data-grid/src/components/GridRow.tsx Outdated Show resolved Hide resolved
packages/grid/x-data-grid/src/components/GridRow.tsx Outdated Show resolved Hide resolved
Copy link
Member

@MBilalShafi MBilalShafi left a comment

Choose a reason for hiding this comment

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

Great improvement 🚀

docs/data/data-grid/performance/performance.md Outdated Show resolved Hide resolved
docs/data/data-grid/performance/performance.md Outdated Show resolved Hide resolved
docs/data/data-grid/performance/performance.md Outdated Show resolved Hide resolved
docs/data/data-grid/performance/performance.md Outdated Show resolved Hide resolved
m4theushw and others added 2 commits February 27, 2023 18:58
Co-authored-by: Andrew Cherniavskii <andrew.cherniavskii@gmail.com>
Signed-off-by: Matheus Wichman <matheushw@outlook.com>
@m4theushw m4theushw merged commit 13c7b10 into mui:next Feb 27, 2023
@m4theushw m4theushw deleted the memo-rows-cells branch February 27, 2023 22:49
Comment on lines +50 to +54
'&&& .updating': (theme) => ({
background: teal[theme.palette.mode === 'dark' ? 900 : 100],
transition: theme.transitions.create('background', {
duration: theme.transitions.duration.standard,
}),
Copy link
Member

Choose a reason for hiding this comment

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

I love the idea, but I think that we could revisit the animation. Done in #8986 😁

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: data grid This is the name of the generic UI component, not the React module! performance
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants