Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
cb1539b
perf: initial work
romgrk Oct 29, 2024
be47607
perf: more work
romgrk Oct 29, 2024
e387422
lint
romgrk Oct 29, 2024
2da8c80
lint
romgrk Oct 29, 2024
5cf7676
lint
romgrk Oct 29, 2024
f65132c
lint
romgrk Oct 29, 2024
490e808
lint
romgrk Oct 29, 2024
17eb786
lint
romgrk Oct 29, 2024
6d2f759
lint
romgrk Oct 30, 2024
4f7de94
lint
romgrk Oct 30, 2024
89d3e11
perf: more work
romgrk Oct 30, 2024
d88d4db
lint
romgrk Oct 30, 2024
c288aac
perf: remove allocations
romgrk Oct 30, 2024
7d70f97
lint
romgrk Oct 30, 2024
a221c46
perf: container queries
romgrk Oct 30, 2024
b1cdfcb
lint
romgrk Oct 30, 2024
d4141ca
Merge branch 'master' into perf-sx
romgrk Nov 1, 2024
f7fe570
lint
romgrk Nov 1, 2024
ec67cbb
lint
romgrk Nov 1, 2024
5ef7075
perf: compose()
romgrk Nov 1, 2024
0123e02
refactor: alternateProp logic for fontFamilyCode
romgrk Nov 1, 2024
07e4410
lint
romgrk Nov 1, 2024
f202bc8
lint
romgrk Nov 1, 2024
d9f7017
lint
romgrk Nov 1, 2024
046d66b
lint
romgrk Nov 1, 2024
d586a77
perf: improve breakpoint mq cloning
romgrk Nov 1, 2024
8330b43
lint
romgrk Nov 1, 2024
b9642a7
fix: breakpoints
romgrk Nov 1, 2024
520a5d2
lint
romgrk Nov 1, 2024
096c50b
lint
romgrk Nov 1, 2024
f3749aa
lint
romgrk Nov 1, 2024
3171351
lint
romgrk Nov 1, 2024
7e0e0c1
lint
romgrk Nov 1, 2024
fff7e0b
perf: spacing
romgrk Nov 1, 2024
40cc09d
lint
romgrk Nov 1, 2024
c1fb261
fix: tests
romgrk Nov 1, 2024
739f020
lint
romgrk Nov 1, 2024
2d09be5
refactor: remove EMPTY_BREAKPOINTS
romgrk Nov 1, 2024
2874595
fix
romgrk Nov 1, 2024
e3de9d8
lint
romgrk Nov 1, 2024
6eac84f
lint
romgrk Nov 1, 2024
8790800
lint
romgrk Nov 1, 2024
9d995d0
lint
romgrk Nov 1, 2024
013e87d
lint
romgrk Nov 1, 2024
ec1914a
lint
romgrk Nov 1, 2024
6c32c64
refactor: hasBreakpoint
romgrk Nov 1, 2024
e6f2319
lint
romgrk Nov 1, 2024
db32450
lint
romgrk Nov 2, 2024
b120356
lint
romgrk Nov 2, 2024
aa57a19
refactor: removeUnusedBreakpoints
romgrk Nov 2, 2024
0b6c30d
refactor: iterateBreakpoints
romgrk Nov 2, 2024
811d63a
refactor: sortContainerQueries
romgrk Nov 2, 2024
419cfc6
lint
romgrk Nov 4, 2024
f096175
lint
romgrk Nov 4, 2024
3455a04
lint
romgrk Nov 4, 2024
260cfd3
Merge branch 'master' into perf-sx
romgrk Feb 17, 2025
6aaf325
Merge branch 'master' into perf-sx
romgrk Apr 29, 2025
6580df8
Merge branch 'perf-sx' of github.com:romgrk/material-ui into perf-sx
romgrk Apr 29, 2025
3ed6417
refactor: fix merge conflict
romgrk Apr 29, 2025
b147af1
Merge branch 'master' into perf-sx
mnajdova Mar 10, 2026
354fa02
fix layers
mnajdova Mar 10, 2026
71fc34e
lint
mnajdova Mar 10, 2026
f89cd27
Merge branch 'master' into perf-sx
mnajdova Mar 11, 2026
f5b6e62
more fixes
mnajdova Mar 11, 2026
c893cb0
add null check
mnajdova Mar 24, 2026
12ef81e
Merge branch 'master' into perf-sx
mnajdova Mar 24, 2026
f5c2de2
ci
mnajdova Mar 24, 2026
a4dfff1
ci
mnajdova Mar 24, 2026
8cc7e52
Merge branch 'master' into perf-sx
mnajdova Mar 24, 2026
c24e66e
Fix internal_mediaKeys type and container query shorthands for non-co…
siriwatknp Mar 25, 2026
b6b60f0
Merge branch 'master' into perf-sx
siriwatknp Mar 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/mui-material/src/styles/createThemeWithVars.js
Original file line number Diff line number Diff line change
Expand Up @@ -984,6 +984,7 @@ export default function createThemeWithVars(options = {}, ...args) {
theme: this,
});
};
theme.internal_cache = {};
theme.toRuntimeSource = stringifyTheme; // for Pigment CSS integration

return theme;
Expand Down
2 changes: 1 addition & 1 deletion packages/mui-material/src/styles/stringifyTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function stringifyTheme(baseTheme: Record<string, any> = {}) {
// eslint-disable-next-line no-plusplus
for (let index = 0; index < array.length; index++) {
const [key, value] = array[index];
if (!isSerializable(value) || key.startsWith('unstable_')) {
if (!isSerializable(value) || key.startsWith('unstable_') || key.startsWith('internal_')) {
delete object[key];
} else if (isPlainObject(value)) {
object[key] = { ...value };
Expand Down
13 changes: 12 additions & 1 deletion packages/mui-system/src/breakpoints/breakpoints.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { CSSObject } from '@mui/styled-engine';
import { Breakpoints } from '../createBreakpoints/createBreakpoints';
import type { Breakpoint } from '../createTheme';
import type { Breakpoint, Theme } from '../createTheme';
import { ResponsiveStyleValue } from '../styleFunctionSx';
import { StyleFunction } from '../style';

export const DEFAULT_BREAKPOINTS: Breakpoints;

export interface ResolveBreakpointValuesOptions<T> {
values: ResponsiveStyleValue<T>;
breakpoints?: Breakpoints['values'] | undefined;
Expand All @@ -15,12 +17,21 @@ export function resolveBreakpointValues<T>(

export function mergeBreakpointsInOrder(breakpoints: Breakpoints, styles: CSSObject[]): CSSObject;

export function iterateBreakpoints(
target: any,
theme: Theme,
propValue: any,
callback: (mediaKey: string | undefined, value: any, initialKey?: string) => any,
): any;

export function handleBreakpoints<Props>(
props: Props,
propValue: any,
styleFromPropValue: (value: any, breakpoint?: Breakpoint) => any,
): any;

export function hasBreakpoint(breakpoints: Breakpoints, value: any): boolean;

type DefaultBreakPoints = 'xs' | 'sm' | 'md' | 'lg' | 'xl';

/**
Expand Down
146 changes: 97 additions & 49 deletions packages/mui-system/src/breakpoints/breakpoints.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import PropTypes from 'prop-types';
import isObjectEmpty from '@mui/utils/isObjectEmpty';
import fastDeepAssign from '@mui/utils/fastDeepAssign';
import deepmerge from '@mui/utils/deepmerge';
import merge from '../merge';
import { isCqShorthand, getContainerQuery } from '../cssContainerQueries';
import createBreakpoints from '../createBreakpoints/createBreakpoints';

const EMPTY_THEME = {};

// The breakpoint **start** at this value.
// For instance with the first breakpoint xs: [xs, sm[.
Expand All @@ -13,12 +18,7 @@ export const values = {
xl: 1536, // large screen
};

const defaultBreakpoints = {
// Sorted ASC by size. That's important.
// It can't be configured as it's used statically for propTypes.
keys: ['xs', 'sm', 'md', 'lg', 'xl'],
up: (key) => `@media (min-width:${values[key]}px)`,
};
export const DEFAULT_BREAKPOINTS = createBreakpoints({ values });

const defaultContainerQueries = {
containerQueries: (containerName) => ({
Expand All @@ -35,52 +35,77 @@ const defaultContainerQueries = {
};

export function handleBreakpoints(props, propValue, styleFromPropValue) {
const theme = props.theme || {};
const result = {};
return iterateBreakpoints(result, props.theme, propValue, (mediaKey, value, initialKey) => {
const finalValue = styleFromPropValue(value, initialKey);
if (mediaKey) {
result[mediaKey] = finalValue;
} else {
fastDeepAssign(result, finalValue);
}
});
}

export function iterateBreakpoints(target, theme, propValue, callback) {
theme ??= EMPTY_THEME;

if (Array.isArray(propValue)) {
const themeBreakpoints = theme.breakpoints || defaultBreakpoints;
return propValue.reduce((acc, item, index) => {
acc[themeBreakpoints.up(themeBreakpoints.keys[index])] = styleFromPropValue(propValue[index]);
return acc;
}, {});
const breakpoints = theme.breakpoints ?? DEFAULT_BREAKPOINTS;
for (let i = 0; i < propValue.length; i += 1) {
buildBreakpoint(
target,
breakpoints.up(breakpoints.keys[i]),
propValue[i],
undefined,
callback,
);
}
return target;
}

if (typeof propValue === 'object') {
const themeBreakpoints = theme.breakpoints || defaultBreakpoints;
return Object.keys(propValue).reduce((acc, breakpoint) => {
if (isCqShorthand(themeBreakpoints.keys, breakpoint)) {
const breakpoints = theme.breakpoints ?? DEFAULT_BREAKPOINTS;
const breakpointValues = breakpoints.values ?? values;

for (const key in propValue) {
if (isCqShorthand(breakpoints.keys, key)) {
const containerKey = getContainerQuery(
theme.containerQueries ? theme : defaultContainerQueries,
breakpoint,
key,
);
if (containerKey) {
acc[containerKey] = styleFromPropValue(propValue[breakpoint], breakpoint);
buildBreakpoint(target, containerKey, propValue[key], key, callback);
}
}
// key is breakpoint
else if (Object.keys(themeBreakpoints.values || values).includes(breakpoint)) {
const mediaKey = themeBreakpoints.up(breakpoint);
acc[mediaKey] = styleFromPropValue(propValue[breakpoint], breakpoint);
// key is key
else if (key in breakpointValues) {
const mediaKey = breakpoints.up(key);
buildBreakpoint(target, mediaKey, propValue[key], key, callback);
} else {
const cssKey = breakpoint;
acc[cssKey] = propValue[cssKey];
const cssKey = key;
target[cssKey] = propValue[cssKey];
}
return acc;
}, {});
}

return target;
}

const output = styleFromPropValue(propValue);
callback(undefined, propValue);

return output;
return target;
}

function breakpoints(styleFunction) {
// false positive
function buildBreakpoint(target, mediaKey, value, initialKey, callback) {
target[mediaKey] ??= {};
callback(mediaKey, value, initialKey);
}

function setupBreakpoints(styleFunction) {
// eslint-disable-next-line react/function-component-definition
const newStyleFunction = (props) => {
const theme = props.theme || {};
const base = styleFunction(props);
const themeBreakpoints = theme.breakpoints || defaultBreakpoints;
const themeBreakpoints = theme.breakpoints || DEFAULT_BREAKPOINTS;

const extended = themeBreakpoints.keys.reduce((acc, key) => {
if (props[key]) {
Expand Down Expand Up @@ -110,33 +135,36 @@ function breakpoints(styleFunction) {
return newStyleFunction;
}

export function createEmptyBreakpointObject(breakpointsInput = {}) {
const breakpointsInOrder = breakpointsInput.keys?.reduce((acc, key) => {
const breakpointStyleKey = breakpointsInput.up(key);
acc[breakpointStyleKey] = {};
return acc;
}, {});
return breakpointsInOrder || {};
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;
Comment on lines +138 to +144
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.
}

export function removeUnusedBreakpoints(breakpointKeys, style) {
return breakpointKeys.reduce((acc, key) => {
const breakpointOutput = acc[key];
const isBreakpointUnused = !breakpointOutput || Object.keys(breakpointOutput).length === 0;
if (isBreakpointUnused) {
delete acc[key];
export function removeUnusedBreakpoints(breakpoints, style) {
const breakpointKeys = breakpoints.internal_mediaKeys;

for (let i = 0; i < breakpointKeys.length; i += 1) {
const key = breakpointKeys[i];

if (isObjectEmpty(style[key])) {
delete style[key];
}
return acc;
}, style);
}

return style;
}

export function mergeBreakpointsInOrder(breakpointsInput, ...styles) {
const emptyBreakpoints = createEmptyBreakpointObject(breakpointsInput);
export function mergeBreakpointsInOrder(breakpoints, ...styles) {
const emptyBreakpoints = createEmptyBreakpointObject(breakpoints);
const mergedOutput = [emptyBreakpoints, ...styles].reduce(
(prev, next) => deepmerge(prev, next),
{},
);
return removeUnusedBreakpoints(Object.keys(emptyBreakpoints), mergedOutput);
return removeUnusedBreakpoints(breakpoints, mergedOutput);
}

// compute base for responsive values; e.g.,
Expand Down Expand Up @@ -197,4 +225,24 @@ export function resolveBreakpointValues({
}, {});
}

export default breakpoints;
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;
}
}
const valueKeys = Object.keys(value);
for (let i = 0; i < valueKeys.length; i += 1) {
if (isCqShorthand(breakpoints.keys, valueKeys[i])) {
return true;
}
}
}
return false;
}
Comment on lines +228 to +246
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.


export default setupBreakpoints;
8 changes: 7 additions & 1 deletion packages/mui-system/src/breakpoints/breakpoints.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,13 @@ describe('breakpoints', () => {
describe('function: removeUnusedBreakpoints', () => {
it('allow value to be null', () => {
const result = removeUnusedBreakpoints(
['@media (min-width:0px)', '@media (min-width:600px)', '@media (min-width:960px)'],
{
internal_mediaKeys: [
'@media (min-width:0px)',
'@media (min-width:600px)',
'@media (min-width:960px)',
],
},
{
'@media (min-width:0px)': {
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
Expand Down
13 changes: 6 additions & 7 deletions packages/mui-system/src/compose/compose.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import merge from '../merge';
import fastDeepAssign from '@mui/utils/fastDeepAssign';

function compose(...styles) {
const handlers = styles.reduce((acc, style) => {
Expand All @@ -9,16 +9,15 @@ function compose(...styles) {
return acc;
}, {});

// false positive
// eslint-disable-next-line react/function-component-definition
const fn = (props) => {
return Object.keys(props).reduce((acc, prop) => {
const result = {};
for (const prop in props) {
if (handlers[prop]) {
return merge(acc, handlers[prop](props));
fastDeepAssign(result, handlers[prop](props));
}

return acc;
}, {});
}
return result;
};

fn.propTypes =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ export interface Breakpoints {
* @default 'px'
*/
unit?: string | undefined;
/**
* Media query keys
* @ignore - Do not document.
*/
internal_mediaKeys: string[];
}

export interface BreakpointsOptions extends Partial<Breakpoints> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ export default function createBreakpoints(breakpoints) {
return between(key, keys[keys.indexOf(key) + 1]).replace('@media', '@media not all and');
}

const mediaKeys = [];
for (let i = 0; i < keys.length; i += 1) {
mediaKeys.push(up(keys[i]));
Comment on lines +81 to +83
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);

}

return {
keys,
values: sortedValues,
Expand All @@ -87,6 +92,7 @@ export default function createBreakpoints(breakpoints) {
only,
not,
unit,
internal_mediaKeys: mediaKeys,
...other,
};
}
9 changes: 1 addition & 8 deletions packages/mui-system/src/createStyled/createStyled.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import styledEngineStyled, {
internal_mutateStyles as mutateStyles,
internal_serializeStyles as serializeStyles,
} from '@mui/styled-engine';
import isObjectEmpty from '@mui/utils/isObjectEmpty';
import { isPlainObject } from '@mui/utils/deepmerge';
import capitalize from '@mui/utils/capitalize';
import getDisplayName from '@mui/utils/getDisplayName';
Expand Down Expand Up @@ -330,14 +331,6 @@ function generateStyledLabel(componentName, componentSlot) {
return label;
}

function isObjectEmpty(object) {
// eslint-disable-next-line
for (const _ in object) {
return false;
}
return true;
}

// https://github.com/emotion-js/emotion/blob/26ded6109fcd8ca9875cc2ce4564fee678a3f3c5/packages/styled/src/utils.js#L40
function isStringTag(tag) {
return (
Expand Down
2 changes: 2 additions & 0 deletions packages/mui-system/src/createTheme/createTheme.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ function createTheme(options = {}, ...args) {
});
};

muiTheme.internal_cache = {};

return muiTheme;
}

Expand Down
Loading
Loading