Skip to content

[internal] perf: sx#44254

Merged
siriwatknp merged 71 commits intomui:masterfrom
romgrk:perf-sx
Mar 25, 2026
Merged

[internal] perf: sx#44254
siriwatknp merged 71 commits intomui:masterfrom
romgrk:perf-sx

Conversation

@romgrk
Copy link
Contributor

@romgrk romgrk commented Oct 29, 2024

Improve sx performance by removing allocations.

Example user code:

Before: 23.4ms +-1.6
After:  15.2ms +-1.1

n=100

We get roughly 35% less rendering time for the example above which makes heavy use of the sx prop, so I guess user-code would see something in the 0-40% less rendering time depending on how much they use that prop.

We can still get a lot more improvement by eliminating more memory allocations (I think we can get that example to at least 50% less rendering time), but the remaining changes would require more substantial work. The format of the style({ [prop]: value, theme }) sx style handlers is expensive, we could instead use something like style(prop, value, theme), though IIUC the system props also use those so there's a bit of refactoring to do.

The logic to create an empty breakpoint object for each sx object/subobject is also expensive, I've tried to remove it by lazily initializing the breakpoints but handleBreakpoints is used too much in the codebase so that's another large refactor.

@romgrk romgrk added performance internal Behind-the-scenes enhancement. Formerly called “core”. labels Oct 29, 2024
@mui-bot
Copy link

mui-bot commented Oct 29, 2024

Netlify deploy preview

https://deploy-preview-44254--material-ui.netlify.app/

Bundle size report

Bundle Parsed size Gzip size
@mui/material 🔺+1.09KB(+0.22%) 🔺+413B(+0.29%)
@mui/lab 🔺+2.09KB(+6.56%) 🔺+788B(+9.78%)
@mui/system 🔺+1.23KB(+1.81%) 🔺+416B(+1.73%)
@mui/utils 🔺+836B(+5.44%) 🔺+264B(+4.40%)

Details of bundle changes

Generated by 🚫 dangerJS against b6b60f0

@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Apr 29, 2025
@siriwatknp
Copy link
Member

I will spend time on this next week.

@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jun 12, 2025
@oliviertassinari oliviertassinari changed the title [core] perf: sx [internal] perf: sx Aug 16, 2025
# Conflicts:
#	docs/data/system/getting-started/custom-components/CombiningStyleFunctionsDemo.js
#	docs/data/system/getting-started/custom-components/CombiningStyleFunctionsDemo.tsx
#	packages/mui-system/src/createStyled/createStyled.js
#	packages/mui-system/src/cssContainerQueries/cssContainerQueries.ts
#	packages/mui-system/src/merge/merge.ts
#	packages/mui-system/src/style/style.d.ts
#	packages/mui-system/src/styleFunctionSx/styleFunctionSx.js
@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Mar 10, 2026
@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Mar 11, 2026
@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Mar 11, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR targets sx runtime performance in @mui/system by reducing allocations and switching to more mutation-friendly helpers during style computation, especially around deep merges and breakpoint traversal.

Changes:

  • Introduces @mui/utils/fastDeepAssign and @mui/utils/isObjectEmpty and adopts them across system utilities.
  • Refactors breakpoint handling (iterateBreakpoints, DEFAULT_BREAKPOINTS, internal_mediaKeys) and updates sx processing to use these faster primitives.
  • Migrates style implementation to TypeScript and updates docs/tests to match the new theme/breakpoint setup.

Reviewed changes

Copilot reviewed 24 out of 27 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/mui-utils/src/isObjectEmpty/isObjectEmpty.ts Adds shared object-emptiness helper used by system utilities
packages/mui-utils/src/fastDeepAssign/fastDeepAssign.ts Adds a performance-focused deep assign/merge helper
packages/mui-utils/src/index.ts Exposes new utils from the utils package entrypoint
packages/mui-system/src/styleFunctionSx/styleFunctionSx.js Refactors sx traversal to reduce allocations and use new breakpoint helpers
packages/mui-system/src/breakpoints/breakpoints.js Adds DEFAULT_BREAKPOINTS, iterateBreakpoints, and uses internal_mediaKeys for faster empty breakpoint objects
packages/mui-system/src/createBreakpoints/createBreakpoints.* Precomputes internal_mediaKeys for reuse
packages/mui-system/src/spacing/spacing.js Reduces allocations by precomputing CSS property mappings and iterating props via for..in
packages/mui-system/src/compose/compose.js Switches compose merging to fastDeepAssign
packages/mui-system/src/cssContainerQueries/cssContainerQueries.ts Optimizes sorting by avoiding intermediate arrays/copies and early-exiting when no CQ keys exist
packages/mui-system/src/style/style.ts (+ removals) Migrates style implementation to TS and adds getStyleValue2/optimized getPath
packages/mui-system/src/createTheme/createTheme.js (+ vars theme files) Adds internal_cache to theme objects
packages/mui-material/src/styles/stringifyTheme.ts Filters internal_* keys from theme stringification
docs/data/system/getting-started/custom-components/CombiningStyleFunctionsDemo.* Updates example preview/source (currently inconsistent)
Comments suppressed due to low confidence (1)

docs/data/system/getting-started/custom-components/CombiningStyleFunctionsDemo.tsx:15

  • This example now imports createTheme but doesn't use it, and the preview wraps the output in <ThemeProvider theme={theme}> even though this component doesn't provide a ThemeProvider or define theme. Either wrap Div with a ThemeProvider here (and create a theme via createTheme()), or revert the preview change to avoid broken docs/examples.
import styled from 'styled-components';
import { palette, PaletteProps, spacing, SpacingProps } from '@mui/system';
import { createTheme } from '@mui/material/styles';

const Div = styled.div<PaletteProps & SpacingProps>`
  ${palette}
  ${spacing}
`;

export default function CombiningStyleFunctionsDemo() {
  return (
    <Div color="white" bgcolor="palevioletred" p={1}>
      Styled components
    </Div>
  );

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +138 to +144
export function createEmptyBreakpointObject(breakpoints = DEFAULT_BREAKPOINTS) {
const { internal_mediaKeys: mediaKeys } = breakpoints;
const result = {};
for (let i = 0; i < mediaKeys.length; i += 1) {
result[mediaKeys[i]] = {};
}
return result;
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

createEmptyBreakpointObject now requires breakpoints.internal_mediaKeys to exist. If a consumer provides a custom theme.breakpoints object (or a test stub) without this internal field, this will throw. Consider falling back to computing media keys from breakpoints.keys + breakpoints.up when internal_mediaKeys is missing, so this remains backward-compatible.

Copilot uses AI. Check for mistakes.
Comment on lines +228 to +240
export function hasBreakpoint(breakpoints, value) {
if (Array.isArray(value)) {
return true;
}
if (typeof value === 'object' && value !== null) {
for (let i = 0; i < breakpoints.keys.length; i += 1) {
if (breakpoints.keys[i] in value) {
return true;
}
}
}
return false;
}
Copy link
Member

Choose a reason for hiding this comment

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

This is, more or less, Array.some

export function hasBreakpoint(breakpoints, value) {
  if (Array.isArray(value)) {
    return true;
  }
  if (typeof value === 'object' && value !== null) {
    return breakpoints.keys.some((key) => key in value);
  }
  return false;
}

Copy link
Member

Choose a reason for hiding this comment

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

Considering this is a perf PR, I assume the changes are intentionally not using array methods, e.g. check https://romgrk.com/posts/optimizing-javascript/#3-avoid-arrayobject-methods. Same for the other comments below 👇

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed intentional. I avoid array methods on very low-level utils like these ones which are used basically everywhere. I only do micro-optizimations like this for very hot code, not so much for high-level components.

Comment on lines +81 to +83
const mediaKeys = [];
for (let i = 0; i < keys.length; i += 1) {
mediaKeys.push(up(keys[i]));
Copy link
Member

Choose a reason for hiding this comment

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

maybe map?

const mediaKeys = keys.map(up);


function hasContainerQuery(css: Record<string, any>) {
for (const key in css) {
if (key.startsWith('@container')) {
Copy link
Member

Choose a reason for hiding this comment

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

some here

Comment on lines +61 to +68
if (hasBreakpoint(breakpoints, value)) {
iterateBreakpoints(css, props.theme, value, (mediaKey, finalValue) => {
css[mediaKey][styleKey] = finalValue;
});
} else {
wrapper.sx = value;
css[styleKey] = styleFunctionSx(wrapper);
}
Copy link
Member

@mj12albert mj12albert Mar 24, 2026

Choose a reason for hiding this comment

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

Codex review – it suggested letting the hasBreakpoint check allow container query shorthands (sx={{ opacity: { '@xs': 0.1, '@sm': 0.2 } }}) because it currently works on master but this PR changes that

Container-query shorthands stopped working for non-system CSS props. The decision point in styleFunctionSx.js now depends on hasBreakpoint(), but that helper only checks breakpoint keys and arrays, not @xs / @600 shorthands. I verified sx={{ opacity: { '@xs': 0.1, '@sm': 0.2 } }}: master produces @container rules, this PR returns the raw nested object unchanged. Recommendation: treat container-query shorthands as breakpoint-like in hasBreakpoint, or restore the old “run through handleBreakpoints first” behavior for unknown object-valued CSS props.

Copy link
Member

@siriwatknp siriwatknp Mar 25, 2026

Choose a reason for hiding this comment

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

pushed a fix and added a test.

siriwatknp and others added 2 commits March 25, 2026 12:02
…nfig CSS props

- Fix internal_mediaKeys type from { [key: string]: number } to string[]
- Fix hasBreakpoint to detect container query shorthands (@xs, @sm, etc.)
  for non-system CSS properties like opacity
- Add regression test for sx={{ opacity: { '@xs': 0.1, '@sm': 0.2 } }}

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@siriwatknp siriwatknp merged commit a884702 into mui:master Mar 25, 2026
19 checks passed
@alexwork1611
Copy link

Huge. Thank you!

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

Labels

internal Behind-the-scenes enhancement. Formerly called “core”. performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants