-
-
Notifications
You must be signed in to change notification settings - Fork 32.7k
[internal] perf: sx #44254
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
[internal] perf: sx #44254
Changes from all commits
cb1539b
be47607
e387422
2da8c80
5cf7676
f65132c
490e808
17eb786
6d2f759
4f7de94
89d3e11
d88d4db
c288aac
7d70f97
a221c46
b1cdfcb
d4141ca
f7fe570
ec67cbb
5ef7075
0123e02
07e4410
f202bc8
d9f7017
046d66b
d586a77
8330b43
b9642a7
520a5d2
096c50b
f3749aa
3171351
7e0e0c1
fff7e0b
40cc09d
c1fb261
739f020
2d09be5
2874595
e3de9d8
6eac84f
8790800
9d995d0
013e87d
ec1914a
6c32c64
e6f2319
db32450
b120356
aa57a19
0b6c30d
811d63a
419cfc6
f096175
3455a04
260cfd3
6aaf325
6580df8
3ed6417
b147af1
354fa02
71fc34e
f89cd27
f5b6e62
c893cb0
12ef81e
f5c2de2
a4dfff1
8cc7e52
c24e66e
b6b60f0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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[. | ||
|
|
@@ -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) => ({ | ||
|
|
@@ -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]) { | ||
|
|
@@ -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; | ||
| } | ||
|
|
||
| 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])) { | ||
mnajdova marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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., | ||
|
|
@@ -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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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;
}
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 👇
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe map? const mediaKeys = keys.map(up); |
||
| } | ||
|
|
||
| return { | ||
| keys, | ||
| values: sortedValues, | ||
|
|
@@ -87,6 +92,7 @@ export default function createBreakpoints(breakpoints) { | |
| only, | ||
| not, | ||
| unit, | ||
| internal_mediaKeys: mediaKeys, | ||
| ...other, | ||
| }; | ||
| } | ||
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.
createEmptyBreakpointObjectnow requiresbreakpoints.internal_mediaKeysto exist. If a consumer provides a customtheme.breakpointsobject (or a test stub) without this internal field, this will throw. Consider falling back to computing media keys frombreakpoints.keys+breakpoints.upwheninternal_mediaKeysis missing, so this remains backward-compatible.