Conversation
Netlify deploy previewhttps://deploy-preview-44254--material-ui.netlify.app/ Bundle size report
|
|
I will spend time on this next week. |
# 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
There was a problem hiding this comment.
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/fastDeepAssignand@mui/utils/isObjectEmptyand adopts them across system utilities. - Refactors breakpoint handling (
iterateBreakpoints,DEFAULT_BREAKPOINTS,internal_mediaKeys) and updatessxprocessing to use these faster primitives. - Migrates
styleimplementation 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
createThemebut doesn't use it, and the preview wraps the output in<ThemeProvider theme={theme}>even though this component doesn't provide aThemeProvideror definetheme. Either wrapDivwith aThemeProviderhere (and create athemeviacreateTheme()), 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.
| 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; |
There was a problem hiding this comment.
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.
docs/data/system/getting-started/custom-components/CombiningStyleFunctionsDemo.tsx.preview
Outdated
Show resolved
Hide resolved
| 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; | ||
| } |
There was a problem hiding this comment.
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;
}There was a problem hiding this comment.
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 👇
There was a problem hiding this comment.
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.
| const mediaKeys = []; | ||
| for (let i = 0; i < keys.length; i += 1) { | ||
| mediaKeys.push(up(keys[i])); |
There was a problem hiding this comment.
maybe map?
const mediaKeys = keys.map(up);|
|
||
| function hasContainerQuery(css: Record<string, any>) { | ||
| for (const key in css) { | ||
| if (key.startsWith('@container')) { |
| if (hasBreakpoint(breakpoints, value)) { | ||
| iterateBreakpoints(css, props.theme, value, (mediaKey, finalValue) => { | ||
| css[mediaKey][styleKey] = finalValue; | ||
| }); | ||
| } else { | ||
| wrapper.sx = value; | ||
| css[styleKey] = styleFunctionSx(wrapper); | ||
| } |
There was a problem hiding this comment.
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/@600shorthands. I verifiedsx={{ opacity: { '@xs': 0.1, '@sm': 0.2 } }}: master produces@containerrules, 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.
There was a problem hiding this comment.
pushed a fix and added a test.
…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>
|
Huge. Thank you! |
Improve
sxperformance by removing allocations.Example user code:
We get roughly 35% less rendering time for the example above which makes heavy use of the
sxprop, 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 likestyle(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
sxobject/subobject is also expensive, I've tried to remove it by lazily initializing the breakpoints buthandleBreakpointsis used too much in the codebase so that's another large refactor.