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

Improve Material-UI performance #10778

Closed
1 task done
Bessonov opened this issue Mar 23, 2018 · 94 comments
Closed
1 task done

Improve Material-UI performance #10778

Bessonov opened this issue Mar 23, 2018 · 94 comments

Comments

@Bessonov
Copy link

First, thank you very much for this awesome component library! It's great!

I added a drawer in my new app. Mostly, I copypasted drawer example. Just for PoC I multiplied

        <Divider />
        <List>{mailFolderListItems}</List>

section.

After that it feels very slow, especially on mobile device (nexus 4, cordova with crosswalk 20). I use dev mode, but prod mode doesn't speed up much.

Through react dev tools I noticed that components in mailFolderListItems rendered on every link click in react-router or even menu open. It takes sometime up to 50-60ms to rerender ONE {mailFolderListItems}. I use

const modalProps = {
	keepMounted: true, // Better open performance on mobile.
};

To eliminate uncertainty with other app components, I converted mailFolderListItems to Component and disable rerendering:

class MailFolderListItems extends React.Component<{}, {}> {

	shouldComponentUpdate() {
		return false;
	}

	render() {
		return (
			<List>
				<Link to={Routes.Startpage.path}>
					<ListItem>
						<ListItemIcon>
							<InboxIcon />
						</ListItemIcon>
[...]


				<Divider />
				<MailFolderListItems />

After that this part feels OK.

I found #5628 . I suggest to revisit it. Optimizing shouldComponentUpdate is fundamental and most important step to gain performance. PureComponent is just most common shortcut.

Furthermore, I noticed that very much time (1-2ms and more for EVERY material-ui component) is spended in WithStyles.

  • I have searched the issues of this repository and believe that this is not a duplicate.

Expected Behavior

I'm expecting to get most of possible react performance for this great library.

Current Behavior

The app get slower with every material-ui component.

Steps to Reproduce (for bugs)

I don't provide reproducing example yet, because I just copypasted from component demo page, but if needed I can provide codesandbox demo. For browser it's noticeable, if browser slowed down by factor >=5x in performance setting.

Your Environment

Tech Version
Material-UI 1.0.0-beta.38
Material-UI Icons 1.0.0-beta.36
React 16.2.0
browser cordova crosswalk 20 (equals android chrome 50)
@oliviertassinari
Copy link
Member

I'm expecting to get most of possible react performance for this great library.

@Bessonov Performance will be a focus post v1 release. We have tried to keep the core of the library as fast as possible. It should be good enough for +90% of the cases. At least, it's my experience using the library so far. You haven't provided any external reference, aside from raising our attention around the fact that performance is important, we can't make this issue actionable. If you are able to identify a performance root limitation of Material-UI that we can investigate (with a reproduction codesandbox or repository), please raise it. Until then, I'm closing the issue.

@Bessonov
Copy link
Author

Bessonov commented Mar 23, 2018

@oliviertassinari thank you for your quick response! I'm very happy to hear, that performance will be focus after release.

It should be good enough for +90% of the cases. At least, it's my experience using the library so far.

On desktop - yes, it is. But on mobile it is really slow. Just Drawer and some buttons makes app laggy. It's not responsive and consumes more power as needed.

You haven't provided any external reference, aside from raising our attention around the fact that performance is important, we can't make this issue actionable.

I provided a reference to already raised issue and a reference to react documentation.

If you are able to identify a performance root limitation of Material-UI that we can investigate (with a reproduction codesandbox or repository)

As I said, I can do it. Here is a comparison of pure and non pure component. Steps to reproduce are:

  1. Use chrome and go to https://codesandbox.io/s/r1ov818nwm
  2. Click "Open in a new window" in preview window
  3. Open dev tools and go to "performance" tab
  4. Throttle your CPU by at least 5x (depending on your PC - may be 10x) to reflect mobile device
  5. Click on burger and see how it lags.

And now:

  1. Go to index.js
  2. change pure to true
  3. Save
  4. Repeat steps from above
  5. Enjoy it!

This example is a litte bit unrealistic because I've inserted too much List elements. But as I said, on mobile it get very quickly to point where you "feel" every action.

@Bessonov
Copy link
Author

Here is my issue with WithStyles. It's on Desktop with CPU throttling. I would like to post screenshots from real device on Monday.

Time for WithStyles(ListItem):
image

Time for ListItem:
image

The difference is 1.26 ms just for a ListItem component. With 13 components like this you can't render with 60fps anymore. But I noticed on Desktop, that this duration is not always here.

@Bessonov
Copy link
Author

Bessonov commented Mar 23, 2018

Here is a comparison of pure and non pure component

Btw I don't say that PureComponent is THE solution for all performance issues. I just say, that it can be one of the helpers to make material-ui usable on mobile.

@oliviertassinari
Copy link
Member

oliviertassinari commented Mar 24, 2018

The difference is 1.26 ms just for a ListItem component.

@Bessonov The WithStyles component should be very cheap for repetitive instance of the same component. We only inject the CSS once.
Maybe you are reaching React limitations and the cost of higher order components.
There is a range of solution to mitigate performance issue in React. For instance, you can use element memoization and virtualization. I'm keeping this issue open as the start point for future performance investigation.
I don't think there is much we can do about it right now. Thanks for raising the concern.

@Bessonov
Copy link
Author

Bessonov commented Mar 24, 2018

Ok, that's only one part. What do you think about more important part - optimizing shouldComponentUpdate?

EDIT: I want to split this issue in two parts. This part is about shouldComponentUpdate and the other part for WithStyles, if I can show more information. Is that OK for you?

@oliviertassinari
Copy link
Member

oliviertassinari commented Mar 24, 2018

optimizing shouldComponentUpdate

@Bessonov We don't implement such logic on purpose on Material-UI side for two reasons:

  1. Most of our components accept react elements, a react element reference change at each render, making the logic systematically waste CPU cycles.
  2. The closer the shouldComponentUpdate logic is from the root of the React tree, the more efficient it's. I mean, the more pruning you can do on the tree, the better. Material-UI components are low level. There low leverage opportunity.

The only opportunity we have found is with the icons. Let us know if you can identify new ones.

the other part for WithStyles

+90% of the time spent in WithStyles is actually spent in JSS, there very few we can do about it on Material-UI side.
At least, I have been identifying a caching opportunity for server-side rendering lately to greatly improve the performance on repetitive rendering. But it's server-side only.

@oliviertassinari oliviertassinari changed the title Re-Rendering is slow Improve Material-UI performance Mar 24, 2018
@Bessonov
Copy link
Author

Bessonov commented Mar 24, 2018

Most of our components accept react elements, a react element reference change at each render, making the logic systematically waste CPU cycles.

That's depending on usage and can be improved. I don't benchmark myself, but I'm sure that right component usage and optimized shouldComponentUpdate (with shallow comparison) is less costly than not optimized component, especially for immutable structures (and immutable.js isn't required btw). Related ticket: #4305

For example in https://github.com/mui-org/material-ui/blob/v1-beta/docs/src/pages/demos/drawers/PermanentDrawer.js#L68 this leads to new objects and makes PureComponent senseless:

        classes={{
          paper: classes.drawerPaper,
        }}

because it returning every time a new object. But not:

const drawerClasses = {
    paper: classes.drawerPaper,
};
[...]
        classes={drawerClasses}

Same for components.

The closer the shouldComponentUpdate logic is from the root of the React tree, the more efficient it's. I mean, the more pruning you can do on the tree, the better.

Yes, that's true.

Material-UI components are low level. There low leverage opportunity.

That's partially true. As you can see in https://codesandbox.io/s/r1ov818nwm I just wrap List in PureComponent. Nothing else and it speed up Drawer by significant factor. I would claim that this is true for every component, at least which use {this.props.children}. I created another demo: https://codesandbox.io/s/my7rmo2m4y

There is just 101 Buttons (pure = false) and Buttons wrapped in PureComponent (pure = true, see where Button import from). Same game result, if I click the "Click"-Button:

Normal Button:
image

Wrapped Button (with overhead and so on):
image

As you can see, there is 637ms vs. 47ms for just normal Buttons! Do you still think that optimize shouldComponentUpdate (or just PureComponent) not worth it? :)

EDIT: fix wording at beginning

@Bessonov
Copy link
Author

Bessonov commented Mar 24, 2018

@oliviertassinari @oreqizer I noticed interesting thing. It seems like extends PureComponent performs better as Component with

  shouldComponentUpdate() {
    return false;
  }

image

EDIT: I think this is one of react internal optimization.

@oliviertassinari
Copy link
Member

oliviertassinari commented Mar 26, 2018

As you can see, there is 637ms vs. 47ms for just normal Buttons! Do you still think that optimize shouldComponentUpdate (or just PureComponent) not worth it? :)

@Bessonov It's demonstrating the potential of the pure logic. Yes, it can be very useful! It's a x13 improvement. But I don't think it's close to real-life conditions:

  • Instead of 100 buttons, you will find 10 buttons at most on a page. Still, you will find 10 grids, 10 X, etc.
  • Instead of simple properties provided to the lower level elements, you will use something like react-intl, the check between the previous and new props will always be false. You will waste CPU cycles. So x13 -> x0.8 or so

@Bessonov
Copy link
Author

@oliviertassinari as a great developer, how you can write things like this? Why you use your personal assumptions as arguments and no facts? I think, I presented enough facts above. I did everything to show it, because I like this project and want to contribute. It's not enough? Ok, then more facts and no assumptions.

Just for you I reduce to 10 Buttons. 10! It's mean any 10 components (worse: containers) from material-ui slows down entire app until the app is not usable! Tested on real device with crosswalk 21/chrome 51 without any throttle:

Normal button:
image

PureButton:
image

This still an 8x improvement! It's huge! Can you imagine how important this on mobile device is?

Instead of 100 buttons, you will find 10 buttons at most on a page. Still, you will find 10 grids, 10 X, etc.

I used Button because it is a one of simplest component! It shows that material-ui is broken from performance point of view. Now, imagine a container components like a AppBar, Toolbar, List, Drawer! This is even worse! You get very quickly to 20 components/containers on every page. And because you don't expire any slowness on your powerful desktop/mac it's not mean that material-ui isn't incredible slow.

react-intl, the check between the previous and new props will always be false. You will waste CPU cycles. So x13 -> x0.8

Show me an example on codesandbox. I don't see why this should be happen. I'm sure, this happens only on wrong usage of react components. And official example shows wrong usage, because it seem like react-intl not applied context subscribers. But there is a lot of other components which are aligned with react guidelines and best practices to stay performant.

BTW: WithStyles consume up to 2,27ms for the Button on mobile device. 8 components and you under 60fps.

@oliviertassinari
Copy link
Member

oliviertassinari commented Mar 27, 2018

Why you use your personal assumptions as arguments and no facts?

What makes you think it's a personal assumption? I was trying to perform critical thinking. Conceptually speaking, the extra prop traversal will slow down the pure version compared to the non pure version, it has to prune something to worth it. Same reasoning as #5628 or react-bootstrap/react-bootstrap#633 (comment) or reactstrap/reactstrap#771 (comment)

With pure:
capture d ecran 2018-03-27 a 11 43 41

Without pure:
capture d ecran 2018-03-27 a 11 44 15

The reproduction is the following.

@Bessonov
Copy link
Author

@oliviertassinari are you sure that codesandbox makes everything right for your test? Because my results are very different:

Without pure (even without throttle it's slow):
image

Pure (after change to true and save I get new url for codesandbox):
image

@Bessonov
Copy link
Author

Bessonov commented Mar 27, 2018

Since it doesn't check context changes and also forces users to ensure all of the children components are also "pure", I don't believe this to be a good fit for this library. This library needs to be as flexible as possible and I believe this will make it more difficult to use.

I see the point. But it's a very interesting tradeoff. On one side even material-ui should be "as flexible as possible", but on the other side current performance makes it unusable at all.

May be you should think about providing pure and non-pure version of components. I do it now for my app to get some usable perfomance even on desktop.

@oliviertassinari
Copy link
Member

oliviertassinari commented Mar 27, 2018

@Bessonov The codesandbox wasn't saved correctly. Sorry. It was missing the following diff. I have updated the link:

<Button>
+ <i>
    Button
+ </i>
</Button>

@Bessonov
Copy link
Author

Bessonov commented Mar 27, 2018

I don't understand why it should produce different results? I get better chart, but non-pure version is significantly slower.

EDIT: ok, I see. Try to figure out what going on...

@Bessonov
Copy link
Author

OK, I understand now. Just the same "new object on every render"-thing. I didn't noticed it before. For some cases it can be improved with constants trough babel plugin automatically.

@Bessonov
Copy link
Author

Well, then you know it already! :D Even there is not much benefit on it's own (you showed ~7%), it's still profitable in conclusion with pure components to avoid some flaws you mention above. I tested it now with Pure Wrapper + babel plugin + production build and it's impressive fast on the same mobile device!

As I said, I think it's best to offer both - non pure components for flexibility and pure components (wrappers are enough to keep it simple and maintable) for performance. But for me, I can live with just pure components, because overall performance improvement is much greater that performance drawbacks. Or better: I can't use material-ui without pure components.

Ok, for now I'm waiting for further input on this topic and create own wrappers in my app )

Thanks for insights!

@dantman
Copy link
Contributor

dantman commented Mar 27, 2018

I've never heard of transform-react-constant-elements actually being used and being really useful. It's ok as as random micro-optimization to throw in, but in practice you rarely write code simple enough to get much from it. Although, I suppose it wouldn't be a bad optimization for all the SVG-style icon components like <Add />.

Take a look at this example (click on Plugins in the side, search for "react-constant" and click the checkbox on "transform-react-constant-elements") and you'll see that barely anything has been optimized:

  • The simple static InputAdornment has been moved to the top, yay.
  • But the trivially simple submit button was no longer optimized the moment we put an event handler on it.
  • And while the InputAdornment itself is now hoisted, the InputProps={{startAdornment: ...}} is still inline and creating a new object every render which makes PureComponent impossible.
  • Likewise classes={{label: classes.runButtonLabel}} is making it impossible for PureComponent to optimize the Run button.

I personally like PureComponent and try to use it absolutely everywhere and optimize things as much as I can so that it works. But it doesn't look like MUI is at all made in a way that PureComponent would work.

  • The *Props props like InputProps is a fundamental pattern to how MUI works. It's not just an advanced way to modify MUI internals when you need it, but rather something that is used regularly in simple use cases. This pattern often makes any leaf component that could normally be optimized in pure mode, unoptimizable.
  • Likewise the classes={{...}} pattern also doesn't work with PureComponent and it is the way to add any styling to things in MUI. (And saying to use classes={classes} is in no way practical because a real life consumer likely has a different class name than the component's internal class and classes is likely to also include classes meant to style other elements in the same consuming component)
  • Children are used a lot, instead of having 1 component a common pattern often makes use of 2-3 and only one of them can be pure optimized if any of them can.

If we want to optimize anything, these fundamental issues need to be dealt with, otherwise simply letting people enable a pure mode MUI won't actually optimize much at all. I can think of two possibilities.

We just allow pure mode to be enabled, consumers have to memoize or hoist objects manually to actually get optimizations

  1. One way I can think of to do this would be some short of shallowMemoize which using the local this and a key (so you can use it on different bits of data) that memoizes the data as long as it's shallow equal
  2. Another I can think of is using something like reselect to create selectors that will memoize data.
  3. Another way of doing the shallowMemoize in 1. would be to pass it to render() with a decorator. This way we could pass in a new one each render and instead of needing key we can just check if any of the memoized objects from the last render should be re-used, then discard all the old values.

The problem of course is this makes consumers a lot larger and messier and requires logic to be manually hoisted up to places where it's far away from the code using it.

import {createSelector} from 'reselect';

class FormPage extends PureComponent {
  state = { example: '' };

  change = e => this.setState({example: e.target.value});
  submit = () => {
    console.log('Submit: ', this.state.example);
  };

  runButtonClasses = createSelector(
    props => props.classes.runButtonLabel,
    runButtonLabel => ({runButtonLabel}));

  render() {
    const {title} = this.props;
    const {example} = this.state;

    return (
      <form>
        {title}
        <TextField
          InputProps={this.shallowMemoize('a', {
            // This example assumes use of transform-react-constant-elements to make this object always the same
            startAdornment: <InputAdornment position="start">Kg</InputAdornment>,
          }}}
          onChange={example}
          value={example} />
        <Button classes={this.runButtonClasses(classes)}>Run</Button>
        <Button onClick={this.submit}>Submit</Button>
      </form>
    );
  }
}
// ...
  @withShallowMemoize
  render(memo) {
    const {title} = this.props;
    const {example} = this.state;

    return (
      <form>
        {title}
        <TextField
          InputProps={memo({
            startAdornment: <InputAdornment position="start">Kg</InputAdornment>,
          }}}
          onChange={example}
          value={example} />
        <Button classes={memo(classes)}>Run</Button>
        <Button onClick={this.submit}>Submit</Button>
      </form>
    );
  }

Instead of trying to opimize InputProps, classes, etc... we encourage people to make micro-components for all of their use casses

If this is the recommended way to use MUI, we may not even need a pure mode. As you can see, as soon as you start making small helper components for your common use cases those components themselves can easily be pure components. In the example WeightTextField now never get's re-rendered as long as value is still the same, completely skipping withStyles, any memoization work necessary for InputProps, or the InputAdornment setup. And when value does change, we have to re-render TextField anyways so the non-pure InputProps={{...}} doesn't matter.

I'm fine with this path. I like micro-components in theory; though I hate every currently valid syntax/pattern for writing them that I can think of. I don't want to MyComponent = enhance(MyComponent), I want to decorate them, but you can't decorate any of the short ways of writing a tiny component. I also dislike turning import {TextField} from 'material-ui'; into import WeightTextField from '../../../ui/WeightTextField;`.

let WeightTextField = ({unit, InputProps, ...props}) => (
  <TextField
    {...props}
    InputProps={{
      startAdornment: <InputAdornment position="start">{unit}</InputAdornment>,
      ...InputProps
    }}
    onChange={example}
    value={example} />
);
WeightTextField = pure(WeightTextField);

RunButton = compose(
  withStyles(theme => ({
    label: {
      fontWeight: '800',
    },
  })),
  pure
)(Button);

const SubmitButton = pure(Button);

class FormPage extends Component {
    state = { example: '' };

  change = e => this.setState({example: e.target.value});
  submit = () => {
      console.log('Submit: ', this.state.example);
    };

  render() {
    const {title} = this.props;
    const {example} = this.state;

    return (
      <form>
        {title}
        <WeightTextField
          unit='Kg'
          onChange={example}
          value={example} />
        <RunButton>Run</RunButton>
        <SubmitButton onClick={this.submit}>Submit</SubmitButton>
      </form>
    );
  }
}

@wilsonjackson
Copy link

I've got a use case where I need display 500–2000 checkboxes on a page in a big list. Using native browser checkboxes, performance is fine, but using the <Checkbox> component, performance is very poor and scales linearly with the number of checkboxes on the page. Example: https://codesandbox.io/s/5x596w5lwn

I'm using mui@next — is there some strategy I can employ now to make this workable?

@dantman
Copy link
Contributor

dantman commented Mar 28, 2018

@wilsonjackson
First, don't do the following. This will create a new handler every checkbox every render, which will deoptimize any PureComponent optimizations you try to do.

  handleChange = index => event => {
    this.setState({

Secondly, make your own small component to wrap Checkbox and make that component pure. This has the extra benefit that you can add in any properties that are common to all your checkboxes. And, since you have the common issue of needing a different change event handler for each item we can use a class component and do that in the component instead of in the list container.

https://codesandbox.io/s/r7l64j6v5n

@oliviertassinari
Copy link
Member

If we want to optimize anything, these fundamental issues need to be dealt with, otherwise simply letting people enable a pure mode MUI won't actually optimize much at all. I can think of two possibilities.

@dantman These API choices have been made in order to improve the DX as much as possible while trying to be fast enough.

Instead of trying to opimize InputProps, classes, etc... we encourage people to make micro-components for all of their use casses

Yes, we do. The wrapping pattern is definitely the encouraged way to customize the library. It can be extended to apply performance optimizations. It's easier on userland where they variability of the usage of the components is much lower. We could even add an FAQ question or guide section about this point in the documentation.

@Pajn
Copy link
Contributor

Pajn commented Mar 16, 2019

Your MyList (and thus List) gets rerendered because of the component that renders MyList (lets call it MyComponent) gets rerendered. PureComponent on MyList does not help as MyComponent have been rerendered and created new children for MyList so that MyLists check fails.

Your MyComponent probably gets rerendered because there is where you store the state of which item is selected.

I think MUIs implementation of List should change to not recreate the List context value each render
here: https://github.com/mui-org/material-ui/blob/fb4889f42613b05220c49f8fc361451066989328/packages/material-ui/src/List/List.js#L57

So instead have List look something like this:

const List = React.forwardRef(function List(props, ref) {
  const {
    children,
    classes,
    className,
    component: Component,
    dense,
    disablePadding,
    subheader,
    ...other
  } = props;
  const context = React.useMemo(() => ({ dense }), [dense]);

  return (
    <Component
      className={clsx(
        classes.root,
        {
          [classes.dense]: dense && !disablePadding,
          [classes.padding]: !disablePadding,
          [classes.subheader]: subheader,
        },
        className,
      )}
      ref={ref}
      {...other}
    >
      <ListContext.Provider value={context}>
        {subheader}
        {children}
      </ListContext.Provider>
    </Component>
  );
});

That would simplify creating sCU versions of the ListItems

@mkermani144
Copy link
Contributor

mkermani144 commented Mar 16, 2019

Your MyList (and thus List) gets rerendered because of the component that renders MyList (lets call it MyComponent) gets rerendered. PureComponent on MyList does not help as MyComponent have been rerendered and created new children for MyList so that MyLists check fails.

@Pajn No, look at my React profiler results. MyList didn't re-render (it's grey), but List did (it's blue). I don't persist on PureComponent for MyList. Even though I implement sCU for MyList so that it doesn't re-render, List does re-render.

@mkermani144
Copy link
Contributor

@oliviertassinari
I created a minimal reproduction example:

import React, { Component } from 'react';

import StylesProvider from '@material-ui/styles/StylesProvider';
import ThemeProvider from '@material-ui/styles/ThemeProvider';

import { createMuiTheme } from '@material-ui/core';

import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';

const theme = createMuiTheme({});

const MyListItem = React.memo(ListItem, (prev, next) => prev.selected === next.selected);

class App extends Component {
  state = {
    selected: null,
  }
  render() {
    return (
      <StylesProvider>
        <ThemeProvider theme={theme}>
          <List>
            {[0, 1, 2, 3, 4].map(el => (
              <MyListItem
                button
                selected={el === this.state.selected}
                onClick={() => this.setState({ selected: el })}
              >
                {el}
              </MyListItem>
            ))}
          </List>
        </ThemeProvider>
      </StylesProvider>
    );
  }
}

export default App;

React profiler results (after clicking on the 4th list item):

image

As you can see, it works as expected, i.e. there is no extra re-renders (except for ButtonBase components inside ListItems). The problem is that, this reproduction is too minimal; there are lots of stuff i skip in it.

I know you cannot tell me what is wrong with my code causing extra re-renders. But I ask you a question: What can cause a re-render in the WithStylesInner components which wrap MUI components?

@oliviertassinari
Copy link
Member

oliviertassinari commented Mar 17, 2019

@mkermani144 What do you think of this fix?

--- a/packages/material-ui/src/List/List.js
+++ b/packages/material-ui/src/List/List.js
@@ -40,6 +40,13 @@ const List = React.forwardRef(function List(props, ref) {
     ...other
   } = props;

+  const context = React.useMemo(
+    () => ({
+      dense,
+    }),
+    [dense],
+  );
+
   return (
     <Component
       className={clsx(
@@ -54,7 +61,7 @@ const List = React.forwardRef(function List(props, ref) {
       ref={ref}
       {...other}
     >
-      <ListContext.Provider value={{ dense }}>
+      <ListContext.Provider value={context}>
         {subheader}
         {children}
       </ListContext.Provider>

Do you want to submit a pull request? :) We are using the same strategy with the Table component. It works great. Thank you for reporting the problem!

@mkermani144
Copy link
Contributor

@oliviertassinari Sure. This is exactly what @Pajn suggested earlier. I submitted a PR.

@mkermani144
Copy link
Contributor

#14934 is merged; the List component, however, may become a performance bottleneck if it's large enough, no matter how much it's optimized. Shouldn't we provide an example showing the usage of react-window or react-virtualized with List component, like the one we have in Table docs?

@eps1lon
Copy link
Member

eps1lon commented Mar 20, 2019

Shouldn't we provide an example showing the usage of react-window or react-virtualized with List component, like the one we have in Table docs?

That'd be great 👍

@henrylearn2rock
Copy link

Fwiw I built a chat app and the app needs to render a large list of contacts. I ran into the same issue
@mkermani144 had.

https://stackoverflow.com/questions/55969987/why-do-the-children-nodes-rerender-when-the-parent-node-is-not-even-being-update/55971559

@oliviertassinari
Copy link
Member

@henrylearn2rock Have you considered using virtualization? We have added a demo for the list: https://next.material-ui.com/demos/lists/#virtualized-list.

@pytyl
Copy link

pytyl commented May 8, 2019

This also really tripped me up. I think most people (including me) assumed everything under a pure component was safe from a rerender, which evidently isn't the case for this library. I am going to try virtualization as you most recently suggested. Thanks!

@eps1lon
Copy link
Member

eps1lon commented May 8, 2019

I think most people (including me) assumed everything under a pure component was safe from a rerender, which evidently isn't the case for this library.

This is not how React.PureComponent or React.memo works. It only affects the the component itself. Children might still have to re-render if context changes.

@pytyl Can you share the code where you used a PureComponent and expected it to prevent any re-render in its sub-tree?

@pytyl
Copy link

pytyl commented May 8, 2019

@eps1lon the following documentation makes it seem like returning false from shouldComponentUpdate automatically skips re-render in children components.
https://reactjs.org/docs/optimizing-performance.html#shouldcomponentupdate-in-action

Since shouldComponentUpdate returned false for the subtree rooted at C2, React did not attempt to render C2, and thus didn’t even have to invoke shouldComponentUpdate on C4 and C5.

Maybe I am mistaken on this? The following is a snapshot of my profiler. Just for the sake of testing, I explicitly return false for shouldComponentUpdate in my Menu component:

Screen Shot 2019-05-08 at 7 46 32 PM

This made all of my children components (Categories, Category, CategoryItems, CategoryItem) not re-render. Many MUI related things appear to be re-rendering at the bottom which seems to be causing a lot of delay. Stuff like withStyles, Typography, ButtonBase. Still a bit new to React so please excuse my ignorance. Below is my code for Menu component (where I am returning false for shouldComponentUpdate):

import React, { Component } from "react";
import Categories from "./Categories";
import { withStyles, Paper } from "@material-ui/core";

const styles = theme => ({
  root: {
    paddingTop: 0,
    marginLeft: theme.spacing.unit * 2,
    marginRight: theme.spacing.unit * 2,
    marginTop: theme.spacing.unit * 1
  }
});

class Menu extends Component {
  shouldComponentUpdate(nextProps, nextState) {
    if (nextProps.categories.length == this.props.categories.length) {
      return false;
    }
    return true;
  }

  render() {
    const { classes, categories } = this.props;

    return (
      <Paper className={classes.root}>
        <Categories categories={categories} />
      </Paper>
    );
  }
}

export default withStyles(styles)(Menu);

@eps1lon
Copy link
Member

eps1lon commented May 9, 2019

I would need a full codesandbox to understand the issue.

@pytyl
Copy link

pytyl commented May 9, 2019

@eps1lon I will try to produce one for tomorrow. Thank you.

@pytyl
Copy link

pytyl commented May 10, 2019

@eps1lon here is the codepen:
https://codesandbox.io/s/348kwwymj5

Short description
It's a basic menu app for restaurants (which often have upwards of 100 menu items). When the user clicks on a menu item, it opens an "add to order" dialog. I will attach a few situations where the profiler is showing poor performance (these statistics are not on a production build).

System
MacBook Pro (Retina, 13-inch, Early 2015)
3.1 GHz Intel Core i7
Firefox 66.0.3

Case 1 (user clicks on a menu item)
Render duration: 218ms

Screen Shot 2019-05-10 at 4 45 26 AM

Screen Shot 2019-05-10 at 4 45 48 AM

Case 2 (user clicks add to order button in dialog)
Render duration: 356ms

Screen Shot 2019-05-10 at 4 46 24 AM

Screen Shot 2019-05-10 at 4 47 10 AM

I'm sure I'm making some novice mistake here so any guidance is greatly appreciated.

@Pajn
Copy link
Contributor

Pajn commented May 10, 2019

As WithStyles(ButtonBase) is rerendered I assume that WithStyles uses a context that is recreated even though it doesn't need to.

I managed to find this https://github.com/mui-org/material-ui/blob/048c9ced0258f38aa38d95d9f1cfa4c7b993a6a5/packages/material-ui-styles/src/StylesProvider/StylesProvider.js#L38 but can't find a place where StylesProvider is used in actual code (GitHubs search isn't very good) but it might be the reason.

Does @eps1lon know if this might be the cause? If it is, a useMemo on the context object would probably fix this. Though I don't know if the localOptions are stable or if useMemo needs to be propagated even further.

@eps1lon
Copy link
Member

eps1lon commented May 10, 2019

Maybe. But you should first investigate why your component using a StylesProvider rerenders. This is either something at the top of your tree or should be at some UI boundary. In either case those should rarely rerender. Remember that react context isn't optimized for frequent updates anyway.

We shouldn't prematurely optimize these things just because some object is re-created during render. Memoization is not a silver bullet. So without a concrete example I can't do much. Yes things re-render. Sometimes more often then they need to. But a wasted re-render doesn't mean it's the cause of a performance bottleneck.

@oliviertassinari
Copy link
Member

oliviertassinari commented May 10, 2019

@pytyl I have looked at your codesandbox, there is a problem with the rendering architecture. You rerender everything when the menu item is clicked. Your GlobalContext jumps the pure logic.

@eps1lon I think that we should close this issue. It would be better to focus on specifically identified issues.

@eps1lon
Copy link
Member

eps1lon commented May 10, 2019

TL;DR: Create context slices, memoize context values, no issue with material-ui specifically: https://codesandbox.io/s/8lx6vk2978

Did some digging and the issue is that you have this big global context that is re-created during render. You re-render your App when you click at which point the global context is re-created. Your CategoryItem is listening to it which appears 100 times in your App. Since you have 100 material-ui MenuItems you encounter the classic death by a thousand cuts.

So ironically part of the solution is memoizing a context value but the important part is identifyng separate context slices. It seems like a state and dispatch context is appropriate. This is recommended when using useContext with useReducer and seems to fit here to.

This can create quite a big tree and render props hell the more contexts you have. I encourage you to have a look at useContext. It will help alot if you start facing these issues.

@oliviertassinari It's good issue to collect common pitfalls with solutions. We can decide if we want to create separate issues from it.

@pytyl
Copy link

pytyl commented May 10, 2019

@oliviertassinari @eps1lon thanks for revising! Performance seems great.

@mankittens
Copy link

mankittens commented Aug 16, 2019

I just had a problem with slow rendering performance. I solved it entirely by replacing all instances of the <Box> component with <div>s. I debugged using the react devtools flamegraph, and I went from around 420ms to 20ms.

With <Box>es;
Screen Shot 2019-08-16 at 12 47 25 AM

Without <Box>es:
Screen Shot 2019-08-16 at 12 42 38 AM

@oliviertassinari
Copy link
Member

@mankittens You could keep the Box component, using styled-components as the style engine. The performance would be much better. It should get better with JSS in the near future #16858.

I'm closing this issue. We need a dedicated performance report for each potential area of improvement, not an umbrella thread.

@CrytoCodeWizard
Copy link

First of all Thanks for the community.
I have some difficulties now by using minimal Material UI theme. This is a simple explanation what I am having.

I am using Minimal Material UI theme to build my user interface now. I am using FormProvider component to add user information.
But I have issue in this, when I submit my data, there is not submit data.
How can I fix this? Please help me and give me solution.

This is my UserModal component :
import * as Yup from 'yup'
import PropTypes from 'prop-types';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import React, { useState, useEffect, forwardRef, useCallback } from "react";

import Slide from '@mui/material/Slide';
import Dialog from '@mui/material/Dialog';
import Button from '@mui/material/Button';
import MenuItem from '@mui/material/MenuItem';
import Grid from '@mui/material/Unstable_Grid2';
import LoadingButton from '@mui/lab/LoadingButton';
import DialogTitle from '@mui/material/DialogTitle';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';

import axios, { endpoints } from 'src/utils/axios';

import FormProvider, {
RHFSelect,
RHFTextField
} from 'src/components/hook-form';

const Transition = forwardRef((props, ref) => <Slide direction="up" ref={ref} {...props} />);

const UserModal = ({ dialog, type, user }) => {
const [customerName, setCustomerName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [webhook, setWebhook] = useState("");
const [casinoWallet, setCasinoWallet] = useState("");
const [withdrawWalletPrivateKey, setWithdrawWalletPrivateKey] = useState("");
const [withdrawWalletAddress, setWithdrawWalletAddress] = useState("");
const [agencyWalletAddress, setAgencyWalletAddress] = useState("");
const [depositPercentageFee, setDepositPercentageFee] = useState(0);
const [withdrawPercentageFee, setWithdrawPercentageFee] = useState(0);
const [fixFee, setFixFee] = useState(0);
const [userRole, setUserRole] = useState("USER");

useEffect(() => {
    if (type === "Create") {
        setCustomerName("");
        setEmail("");
        setWebhook("");
        setUserRole("USER");
    } else if (type === "Update") {
        setCustomerName(user.customerName);
        setEmail(user.email);
        setWebhook(user.webhook);
        setUserRole(user.role);
    }
}, [user, type]);

const UserSchema = Yup.object().shape({
    // customerName: Yup.string().required('Customer Name is required'),
    // email: Yup.string().required('Email is required').email('Email must be a valid email address'),
    // webhook: Yup.string().required('Web Hook is required'),
    // password: Yup.string().required('Password is required'),
});

const userDefaultValues = {
    customerName,
    email,
    password,
    webhook,
    casinoWallet,
    withdrawWalletAddress,
    withdrawWalletPrivateKey,
    agencyWalletAddress,
    depositPercentageFee,
    withdrawPercentageFee,
    fixFee,
    userRole,
}

const methods = useForm({
    resolver: yupResolver(UserSchema),
    userDefaultValues,
});

const {
    reset,
    handleSubmit,
    formState: { isSubmitting },
} = methods;

const onSubmit = handleSubmit((data) => {
    console.log("submit data : ", data);
    if (type === "Created") {
        axios
            .post(endpoints.user.list, data)
            .then(response => {
                console.log(response.data);
                dialog.onFalse();
            })
            .catch(error => {
                console.log("create user error : ", error);
                reset();
            });
    } else if (type === "craere") {
        axios
            .put(`${endpoints.user.list}/${user.id}`, data, {
                headers: {
                    'Content-Type': 'application/json'
                }
            })
            .then(response => {
                console.log(response.data);
                dialog.onFalse();
            })
            .catch(error => {
                console.log("create user error : ", error);
                reset();
            });
    }
});

const handleUserChange = useCallback((event) => {
    const { name, value } = event.target;

    const stateUpdateFunctions = {
        customerName: setCustomerName,
        email: setEmail,
        password: setPassword,
        webhook: setWebhook,
        casinoWallet: setCasinoWallet,
        withdrawWalletAddress: setWithdrawWalletAddress,
        withdrawWalletPrivateKey: setWithdrawWalletPrivateKey,
        agencyWalletAddress: setAgencyWalletAddress,
        depositPercentageFee: setDepositPercentageFee,
        withdrawPercentageFee: setWithdrawPercentageFee,
        fixFee: setFixFee,
        role: setUserRole
    };

    const setState = stateUpdateFunctions[name];
    if (setState) {
        setState(value);
    }
}, []);

return (
    <Dialog
        TransitionComponent={Transition}
        open={dialog.value}
        onClose={dialog.onFalse}>
        <DialogTitle>
            {type === "Create" ? "Create" : "Edit"} User
        </DialogTitle>
        <FormProvider methods={methods} onSubmit={onSubmit}>
            <DialogContent>
                <Grid container spacing={1}>
                    <Grid xs={12} md={6}>
                        <RHFTextField
                            autoFocus
                            fullWidth
                            type="text"
                            name='customerName'
                            margin="dense"
                            variant="outlined"
                            label="Customer Name"
                            onChange={(event) => handleUserChange(event)}
                            value={customerName}
                        />
                    </Grid>
                    <Grid xs={12} md={6}>
                        <RHFTextField
                            fullWidth
                            type="email"
                            name='email'
                            margin="dense"
                            variant="outlined"
                            label="Email Address"
                            onChange={(event) => handleUserChange(event)}
                            value={email}
                        />
                    </Grid>
                    <Grid xs={12} md={6}>
                        <RHFTextField
                            fullWidth
                            type="password"
                            name='password'
                            margin="dense"
                            variant="outlined"
                            label="Password"
                            onChange={(event) => handleUserChange(event)}
                            value={password}
                        />
                    </Grid>
                    <Grid xs={12} md={6}>
                        <RHFTextField
                            fullWidth
                            type="text"
                            name='webhook'
                            margin="dense"
                            variant="outlined"
                            label="Web Hook"
                            onChange={(event) => handleUserChange(event)}
                            value={webhook}
                        />
                    </Grid>
                    <Grid xs={12} md={6}>
                        <RHFTextField
                            fullWidth
                            type="text"
                            name='casinoWallet'
                            margin="dense"
                            variant="outlined"
                            label="Casino Wallet"
                            value={casinoWallet}
                            onChange={(event) => handleUserChange(event)}
                        />
                    </Grid>
                    <Grid xs={12} md={6}>
                        <RHFTextField
                            fullWidth
                            type="text"
                            name='withdrawWalletAddress'
                            margin="dense"
                            variant="outlined"
                            label="Withdraw Wallet Address"
                            value={withdrawWalletAddress}
                            onChange={(event) => handleUserChange(event)}
                        />
                    </Grid>
                    <Grid xs={12} md={6}>
                        <RHFTextField
                            fullWidth
                            type="text"
                            name='withdrawWalletPrivateKey'
                            margin="dense"
                            variant="outlined"
                            label="Withdraw Wallet PrivateKey"
                            value={withdrawWalletPrivateKey}
                            onChange={(event) => handleUserChange(event)}
                        />
                    </Grid>
                    <Grid xs={12} md={6}>
                        <RHFTextField
                            fullWidth
                            type="text"
                            name='agencyWalletAddress'
                            margin="dense"
                            variant="outlined"
                            label="Agency Wallet Address"
                            value={agencyWalletAddress}
                            onChange={(event) => handleUserChange(event)}
                        />
                    </Grid>
                    <Grid xs={12} md={4}>
                        <RHFTextField
                            fullWidth
                            type="number"
                            name='depositPercentageFee'
                            margin="dense"
                            variant="outlined"
                            label="Deposit Percentage Fee"
                            value={depositPercentageFee}
                            onChange={(event) => handleUserChange(event)}
                        />
                    </Grid>
                    <Grid xs={12} md={4}>
                        <RHFTextField
                            fullWidth
                            type="number"
                            name='withdrawPercentageFee'
                            margin="dense"
                            variant="outlined"
                            label="Withdraw Percentage Fee"
                            value={withdrawPercentageFee}
                            onChange={(event) => handleUserChange(event)}
                        />
                    </Grid>
                    <Grid xs={12} md={4}>
                        <RHFTextField
                            fullWidth
                            type="number"
                            name='fixFee'
                            margin="dense"
                            variant="outlined"
                            label="Fix Fee"
                            value={fixFee}
                            onChange={(event) => handleUserChange(event)}
                        />
                    </Grid>
                    <Grid xs={12} md={12}>
                        <RHFSelect
                            variant="outlined"
                            label="User Role"
                            name='role'
                            value={userRole}
                            onChange={(event) => handleUserChange(event)}
                        >
                            {[
                                { value: "USER", label: "User" },
                                { value: "ADMINISTRATOR", label: "Administrator" }
                            ].map((option) => (
                                <MenuItem key={option.value} value={option.value}>
                                    {option.label}
                                </MenuItem>
                            ))}
                        </RHFSelect>
                    </Grid>
                </Grid>
            </DialogContent>
            <DialogActions>
                <Button onClick={dialog.onFalse} variant="outlined" color="warning">
                    Cancel
                </Button>
                <LoadingButton
                    color="success"
                    type="submit"
                    variant="contained"
                    loading={isSubmitting}
                >
                    {type === "Create" ? "Create" : "Update"} User
                </LoadingButton>
            </DialogActions>
        </FormProvider>
    </Dialog>
);

}

UserModal.propTypes = {
dialog: PropTypes.object.isRequired,
type: PropTypes.string.isRequired,
user: PropTypes.object.isRequired,
}

export default UserModal;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests