-
-
Notifications
You must be signed in to change notification settings - Fork 32.3k
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
[colorManipulator][perf] Fast color manipulation #43289
base: master
Are you sure you want to change the base?
Conversation
Netlify deploy previewhttps://deploy-preview-43289--material-ui.netlify.app/ @material-ui/system: parsed: +7.72% , gzip: +9.24% Bundle size reportDetails of bundle changes (Toolpad) |
This comment was marked as resolved.
This comment was marked as resolved.
👍 truly appreciate your effort to make it faster. I have no concern for the implementation but need some alignment for releasing this PR because some utilities, e.g. For the first step (in v6.x), I prefer to keep all of the exported utilities to behave the same (the tests should not change) so that it's not a breaking change for users that are using them. For internal use inside Material UI, we can create a new file inside Material UI that uses the fast version instead (MUI system has to export an unstable prefix). This way, we can have a performance improvement with no breaking changes. cc @mnajdova @DiegoAndai |
I am good with this plan 👌 |
I would have favored doing the switch at once now rather than keeping the two implementations. If we're going to change the color output/behavior, it feels safe/correct to do it in a major. That being said, I have restored I have updated our internal imports to pull from Now that I think about it I've been benchmarking with warmup runs, I should probably also benchmark cold startup time (including codepaths like |
@siriwatknp Ok yeah, maybe it's possible to abstract this in a way that ships with a feature flag.
I think we should start sooner, 90% could feel late to the game vs. other libraries. Would 70% be better?
As long as the min browser support is compatible. But 93% for min browser, this feels too low for enterprise. Would 95% be better?
It's kind of supposed to be a private API: #13039 but everyone uses the source of the Material UI components, to copy them and create their own component, so yeah it's probably used a lot out there.
And I guess also no bundle size increase, so the colorManipulator we export become dead weight? Until MUI X migrates to the new ones? |
I pushed some changes:
|
@romgrk May I have your script to run the perf benchmark using your Test case, or which approach did you take? |
I use this TestCase in a production build of react with 6x CPU slowdown to eliminate timer imprecision on short durations (sub-millisecond is imprecise, so I want test cases that run in >100ms in general). The test case has a edit: And run the benchmark on a clean browser profile (no extensions, in particular react-dev-tools because it adds overhead even for production builds). Also close any program that uses lots of CPU before you run the benchmark. I would have favored keeping the folders separate for the fast and slow code, something like |
I have tried but there are chances that the IDE autocomplete will import So I think it's better for unstable APIs to be exported with a prefix to avoid the chances of being imported by users. |
* @param color Hex color string: #xxx, #xxxxxx, #xxxxxxxx | ||
*/ | ||
export function parseHex(hex: string): Color { | ||
return newColor(...extractHex(hex)); |
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.
If extractHex
allocates a [r, g, b, a]
array to return it, we're paying an allocation cost on each parse()
call, and the gains made here are somewhat lost.
It would be better to keep parseHex
as it was written, and to use the get(Red|Green|Blue)
function to extract the channels from the color in colorChannel
:
// in colorChannel()
if (color.charCodeAt(0) === HASH) {
const c = parseHex(color);
return `${getRed(c)} ${getGreen(c)} ${getBlue(c)}`;
}
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.
If you wonder why there are [r, g, b, a]
arrays in the non-sRGB conversion code, that's because I didn't optimize those colorspaces for performance. It would be possible, but it would make the code harder to read, and considering that code is mathematically dense, and the use-case is rare, I didn't think it was worth 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.
Got it. I will update the code.
Result: Before (v5.15.12): 65.03 ± 4.19 |
The note above about |
The result is still the same ~51.10 ± 3.17. May be with larger set of components will show some difference. |
Maybe there's not much hex parsing, I seem to recall seeing colors in HSL format. Anyway, that's fine, still a good improvement. |
Implementation wise, it looks good to me but we should have a thorough performance benchmark for the latest next and this PR comparing with the bundle size increase (from my quick benchmark, it's about ~2% faster). I will share the result soon (marking this PR as on hold). |
I have noticed that some of our components use color manipulation functions in their style definition, e.g.
alpha(theme.palette.something, 0.5)
. Those functions run everytime the component mounts (and renders I think), but the implementation is very costly. For example,alpha('#00000088', 0.6)
currently callsdecomposeColor x 2
,hexToRGB
andrecomposeColor
. The MUI color representation with{ type: string, values: number[] }
is quite heavy for something that is called as often.A more efficient color representation would be to use a single integer in the range
0x00000000
to0xffffffff
to represent the RGBA channels in a cannonical way, and to operate on that representation with bitwise operators. For example, setting the alpha channel is as simple ascolor & 0xffffff00 | alpha
.edit: Removed outdated benchmarks, updated benchmarks in the next comment.
Test case gist: Mount 20 buttons, 10 switches, 10 text fields.
I would like to propose to replace the color manipulation functions with this new approach. This means that for each CSS color format, we would need to introduce a parser that transforms it into the cannonical representation. This would however also mean that our manipulation functions, although vastly more efficient, would now erase the input color format. This is already the case to some extent, for example we always return input hex format (
#abcdef
) as an RGB color instead (rgb(171, 205, 239)
).The current manipulation functions are public so it would be a breaking change to remove them, but it's also possible to add a new implementation and keep exposing the old functions.