-
-
Notifications
You must be signed in to change notification settings - Fork 32.1k
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
[system] Pre-serialize & cache styles to improve performance #43412
base: master
Are you sure you want to change the base?
Conversation
function styleAttachTheme(props) { | ||
attachTheme(props, themeId, defaultTheme) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have removed the various attachTheme
calls in favor of adding a style function that runs once and attaches the theme directly on the props. It relies on running before anything else.
if (serialized === styles) { | ||
return styles; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is my understanding is correct that this PR only tackles Emotion, not included styled-components.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I tried looking a bit into it but I'm not familiar with their codebase, not sure if the same thing is possible. They need to expose their serialization code and they need to accept pre-serialized objects as input for this optimization to make sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's important to fix styled-components's usage it's less than 10% of the usage: https://npm-stat.com/charts.html?package=%40mui%2Fstyled-engine&package=%40mui%2Fstyled-engine-sc&from=2023-08-29&to=2024-08-29
ea7b780
to
bce2ac0
Compare
|
||
// If `tag` is already a styled component, filter out the `sx` style function | ||
// to prevent unnecessary styles generated by the composite components. | ||
processStyles(tag, (styles) => styles.filter(style => style !== styleFunctionSx)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this mutating tag
though?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@siriwatknp Maybe you could confirm here, I think this line is mutating tag
's style processors, but what we want is instead to skip adding the sx processor to the new styled component. Is that right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regardless of the question above, I think I'll rename this function to internal_mutateStyles
to better express what it's doing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this could be an (existing) issue/bug but I don't have the bandwidth to investigate it and I'm afraid of fixing it and possibly changing the behavior, so I've just renamed internal_processStyles
to internal_mutateStyles
to better highlight what is happening here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we introduced this as an perf improvement as we were processing sx multiple times if we had a styled component invoked over another styled component. This is what I remember about it. Likely @siriwatknp could have more info, but +1 on not changing any behavior as part of this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, @mnajdova is right.
return value; | ||
}; | ||
} | ||
export default memoTheme; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like it shouldn't even be exported #43440
export default memoTheme; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was worried about the typings in the initial PR (memoTheme<T>
), having it here made it easy to include the MaterialTheme
, though material doesn't use much TS from what I can see. I could remove it altogether from material but it would make it less accessible to external users in case they want it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want us to move to a point where we force developers to provide their theme (so we can remove all @mui/system re-exports from @mui/material, so we need a great DX to not rely on a default theme, so this seems OK.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok. So should I remove this file and import unstable_memoTheme
from @mui/system
everywhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is beyond the scope of this PR #43440.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Btw, I added the export from @mui/material because I wanted to use it in the lab, for the LoadingButton.
Netlify deploy previewhttps://deploy-preview-43412--material-ui.netlify.app/ PigmentGrid: parsed: +10.25% , gzip: +8.06% Bundle size reportDetails of bundle changes (Toolpad) |
This prevents the https://github.com/search?q=repo%3Amui%2Fpigment-css%20internal_processStyles&type=code Also weird that the build succeed in one of the previous commits, it should have been failing from the start. |
I could either patch this in this repo with something like: import * as sc from '@mui/styled-engine'
const serialize = sc.internal_serializeStyles ?? x => x
// and ignore the internal_processStyle => internal_mutateStyles refactor Or I could open a PR on pigment and do the changes and wait for it before releasing this one. |
The fix can be localized to this repo. Let me check. |
@romgrk True, but it will be interesting to compare both against Tailwind CSS and CSS Modules. We have to consider that the bundler integration is a friction point for people, e.g. https://x.com/hyeumair/status/1828669441544261970. So I think looking at the performance data, and the cost to make each work will give us a sense of what's most pragmatic. Now, looking at this PR, when I run https://github.com/mui/material-ui/tree/master/benchmark/browser, I don't see much difference. When I compare those, the win is light, if not a bit slower: After: (the big chunk first is Overall, in the test cases,
it suggests that Pigment CSS can be x2 faster than Emotion. |
So the improvement in this PR is that in a case like this:
Before this PR, the Button styles object would be passed 5 times through emotion's
There would be no performance improvement, because there is no work that can be re-used. But on a real-world use-case, it's safe to assume that components will be used multiple times and this will be saving some work. I haven't looked at the implementation of the benchmarks you've linked, but for the "benchmark/browser" one seems to be testing Box & Grid mainly. Box seems to have no default styles that would be affected by this PR, and Grid doesn't have a stable style object because it accesses And for the table benchmark, it might not have big enough style objects for this to matter (e.g. something like Those reasons are why I've been benchmarking/profiling using either the test case I linked in the top-comment or the dashboard template, those are more likely to be representative of what happens for actual users. Relevant sindresorhus blog post.
Maybe, but imho we'd need to get rid of the |
@romgrk This sounds like the same as what I/Oleg implemented in the past: https://github.com/mui/material-ui/blob/v4.x/packages/material-ui-styles/src/makeStyles/makeStyles.js#L77 with makeStyles. It helped a lot 👍. We were using a ref counter, but the problem then became that React 18 started to be unreliable when doing this. We were no longer able to remove <style> element. But then, on the other hand, emotion just doesn't care, they almost keep everything in the DOM (they still have to unmount global styles). So maybe we could have just ignored that problem and we could have made makeStyles work in React 18. It's not great though, the larger the CSSOM the slower rendering becomes.
Source here: https://github.com/mui/material-ui/blob/master/docs/pages/performance/table-mui.js.
I think that this specific benchmark is representative to real case. One of the complaints I heard a lot about in the past was how slow the Table was, meaning how early people had to add pagination or virtualization to keep the rendering time bearable. It's like in #41330: it was fine not to use virtualization in Material UI v4 to render 2,000 SVGs but it's not possible in Material UI v5 anymore, too slow.
This could have potential. With @Janpot, we look at two performance opportunities mui/pigment-css#209 and mui/pigment-css#208. |
This PR uses emotion's serializer to pre-serialize stable styles and re-use them on every render, rather than have emotion pass them through
createStringFromObject
every time.TestCase
Results:
Fix #43440
https://deploy-preview-43412--material-ui.netlify.app/performance/table-mui/