From 5514a6b74b21aa77200dab19cb69dd0a6fc36f0b Mon Sep 17 00:00:00 2001 From: Olivier Tassinari Date: Sat, 7 Nov 2020 16:09:36 +0100 Subject: [PATCH] add a note about performance --- benchmark/browser/README.md | 180 +++--------------- benchmark/browser/index.js | 18 +- .../basic-styled-components/index.js | 27 --- .../browser/scenarios/box-chakra-ui/index.js | 27 ++- .../browser/scenarios/box-emotion/index.js | 26 --- .../scenarios/box-material-ui-styles/index.js | 27 ++- .../scenarios/box-styled-components/index.js | 27 --- .../browser/scenarios/components/index.js | 15 ++ .../browser/scenarios/make-styles/index.js | 34 ++++ .../browser/scenarios/primitives/index.js | 11 ++ .../browser/scenarios/styled-emotion/index.js | 31 +++ .../scenarios/styled-material-ui/index.js | 28 +++ .../browser/scenarios/styled-sc/index.js | 33 ++++ .../sx-prop-box-material-ui/index.js | 8 +- .../scenarios/sx-prop-box-theme-ui/index.js | 2 +- .../scenarios/sx-prop-div-theme-ui/index.js | 2 +- benchmark/browser/scripts/benchmark.js | 50 +++-- docs/src/pages/system/basics/basics.md | 30 ++- 18 files changed, 278 insertions(+), 298 deletions(-) delete mode 100644 benchmark/browser/scenarios/basic-styled-components/index.js delete mode 100644 benchmark/browser/scenarios/box-emotion/index.js delete mode 100644 benchmark/browser/scenarios/box-styled-components/index.js create mode 100644 benchmark/browser/scenarios/components/index.js create mode 100644 benchmark/browser/scenarios/make-styles/index.js create mode 100644 benchmark/browser/scenarios/primitives/index.js create mode 100644 benchmark/browser/scenarios/styled-emotion/index.js create mode 100644 benchmark/browser/scenarios/styled-material-ui/index.js create mode 100644 benchmark/browser/scenarios/styled-sc/index.js diff --git a/benchmark/browser/README.md b/benchmark/browser/README.md index 97b3e69eccdf33..9b73c532d8a380 100644 --- a/benchmark/browser/README.md +++ b/benchmark/browser/README.md @@ -16,163 +16,45 @@ You should use these numbers exclusively for comparing performance between diffe yarn benchmark:browser noop (baseline): - -20.10ms -20.05ms -19.28ms -19.64ms -20.96ms -18.80ms -18.39ms -20.38ms -18.85ms -18.99ms +mean: 4.70ms, median: 4.72ms ------------- -Avg: 19.55ms -Median: 19.46ms - -styled-components Box + @material-ui/system: - -184.17ms -161.51ms -168.84ms -165.21ms -163.43ms -158.10ms -158.81ms -297.91ms -161.25ms -158.87ms +React primitives: +mean: 68.89ms, median: 64.02ms ------------- -Avg: 177.81ms -Median: 162.47ms - -styled-components Box + styled-system: - -142.66ms -146.55ms -141.12ms -141.04ms -139.45ms -145.63ms -141.36ms -134.98ms -123.98ms -146.09ms +React components: +mean: 74.38ms, median: 74.46ms ------------- -Avg: 140.29ms -Median: 141.24ms - -Box emotion: - -143.21ms -135.28ms -122.53ms -124.80ms -143.69ms -147.81ms -138.16ms -124.55ms -140.32ms -157.74ms +Styled Material-UI: +mean: 109.73ms, median: 109.46ms ------------- -Avg: 137.81ms -Median: 139.24ms - -Box @material-ui/styles: - -146.16ms -131.37ms -139.43ms -158.55ms -149.54ms -131.81ms -134.84ms -151.08ms -152.30ms -130.69ms +Styled emotion: +mean: 102.59ms, median: 104.28ms ------------- -Avg: 142.58ms -Median: 142.79ms - -Box styled-components: - -145.59ms -150.12ms -179.04ms -169.63ms -148.21ms -155.55ms -182.55ms -170.04ms -153.14ms -148.92ms +Styled SC: +mean: 104.06ms, median: 102.67ms ------------- -Avg: 160.28ms -Median: 154.35ms - -Basic styled-components box: - -141.73ms -139.71ms -121.01ms -120.02ms -121.81ms -143.22ms -135.67ms -120.85ms -121.08ms -120.59ms +makeStyles: +mean: 93.81ms, median: 92.90ms ------------- -Avg: 128.57ms -Median: 121.44ms - -Chakra-UI box component: - -147.42ms -128.51ms -118.74ms -110.01ms -133.05ms -130.20ms -121.57ms -119.11ms -108.57ms -134.90ms +sx Material-UI box: +mean: 187.98ms, median: 188.77ms ------------- -Avg: 125.21ms -Median: 125.04ms - -Theme-UI box sx prop: - -165.02ms -141.07ms -139.19ms -185.45ms -166.16ms -138.83ms -140.56ms -139.02ms -179.26ms -165.58ms +Box Material-UI: +mean: 159.24ms, median: 157.90ms ------------- -Avg: 156.01ms -Median: 153.05ms - -Theme-UI div sx prop: - -131.07ms -130.84ms -130.99ms -132.66ms -132.24ms -130.89ms -131.11ms -167.10ms -154.42ms -131.48ms +sx Theme-UI box: +mean: 164.22ms, median: 164.16ms +------------- +sx Theme-UI div: +mean: 153.10ms, median: 152.77ms +------------- +Box Chakra-UI: +mean: 154.95ms, median: 153.89ms +------------- +styled-components Box + @material-ui/system: +mean: 176.82ms, median: 176.60ms +------------- +styled-components Box + styled-system: +mean: 155.18ms, median: 154.63ms ------------- -Avg: 137.28ms -Median: 131.30ms -Done in 31.83s. ``` diff --git a/benchmark/browser/index.js b/benchmark/browser/index.js index 4bd1f0b727d96c..f997ff05f770e8 100644 --- a/benchmark/browser/index.js +++ b/benchmark/browser/index.js @@ -15,7 +15,7 @@ const Component = requirePerfScenarios(scenarioSuitePath).default; const start = performance.now(); let end; -function TestCase(props) { +function Measure(props) { const ref = React.useRef(null); React.useLayoutEffect(() => { @@ -28,20 +28,18 @@ function TestCase(props) { }; }); - return ( - -
{props.children}
-
- ); + return
{props.children}
; } -TestCase.propTypes = { +Measure.propTypes = { children: PropTypes.node, }; ReactDOM.render( - - - , + + + + + , rootEl, ); diff --git a/benchmark/browser/scenarios/basic-styled-components/index.js b/benchmark/browser/scenarios/basic-styled-components/index.js deleted file mode 100644 index ec26cbce611919..00000000000000 --- a/benchmark/browser/scenarios/basic-styled-components/index.js +++ /dev/null @@ -1,27 +0,0 @@ -import * as React from 'react'; -import { createMuiTheme } from '@material-ui/core/styles'; -import { spacing } from '@material-ui/system'; -import styledComponents, { - ThemeProvider as StyledComponentsThemeProvider, -} from 'styled-components'; - -const materialSystemTheme = createMuiTheme(); -const BasicStyleComponents = styledComponents('div')(spacing); - -export default function BasicStyledComponents() { - return ( - - {new Array(1000).fill().map(() => ( - - styled-components - - ))} - - ); -} diff --git a/benchmark/browser/scenarios/box-chakra-ui/index.js b/benchmark/browser/scenarios/box-chakra-ui/index.js index 7307bf072e00d5..8b29672f91a5e7 100644 --- a/benchmark/browser/scenarios/box-chakra-ui/index.js +++ b/benchmark/browser/scenarios/box-chakra-ui/index.js @@ -1,16 +1,11 @@ import * as React from 'react'; -import { Box, ThemeProvider, theme } from '@chakra-ui/core'; +import { Box, ThemeProvider } from '@chakra-ui/core'; -// Let's say you want to add custom colors const customTheme = { - ...theme, colors: { - ...theme.colors, - brand: { - 900: '#1a365d', - 800: '#153e75', - 700: '#2a69ac', - }, + text: '#000', + background: '#fff', + primary: '#33e', }, }; @@ -19,13 +14,15 @@ export default function BoxChakraUi() { {new Array(1000).fill().map(() => ( - chakra-ui + test case ))} diff --git a/benchmark/browser/scenarios/box-emotion/index.js b/benchmark/browser/scenarios/box-emotion/index.js deleted file mode 100644 index f7dd839c16ed4d..00000000000000 --- a/benchmark/browser/scenarios/box-emotion/index.js +++ /dev/null @@ -1,26 +0,0 @@ -import * as React from 'react'; -import { createMuiTheme } from '@material-ui/core/styles'; -import styledEmotion from '@emotion/styled'; -import { ThemeProvider as EmotionTheme } from 'emotion-theming'; -import { styleFunction } from '@material-ui/core/Box'; - -const materialSystemTheme = createMuiTheme(); -const Box = styledEmotion('div')(styleFunction); - -export default function BoxEmotion() { - return ( - - {new Array(1000).fill().map(() => ( - - emotion - - ))} - - ); -} diff --git a/benchmark/browser/scenarios/box-material-ui-styles/index.js b/benchmark/browser/scenarios/box-material-ui-styles/index.js index 35ebad0df4b0b8..6eab23af21e65c 100644 --- a/benchmark/browser/scenarios/box-material-ui-styles/index.js +++ b/benchmark/browser/scenarios/box-material-ui-styles/index.js @@ -1,24 +1,21 @@ import * as React from 'react'; -import { createMuiTheme } from '@material-ui/core/styles'; -import { ThemeProvider as StylesThemeProvider } from '@material-ui/styles'; -import BoxStyles from '@material-ui/core/Box'; - -const materialSystemTheme = createMuiTheme(); +import Box from '@material-ui/core/Box'; export default function BoxMaterialUIStyles() { return ( - + {new Array(1000).fill().map(() => ( - - @material-ui/styles - + test case + ))} - + ); } diff --git a/benchmark/browser/scenarios/box-styled-components/index.js b/benchmark/browser/scenarios/box-styled-components/index.js deleted file mode 100644 index f9873fa8f6bcd4..00000000000000 --- a/benchmark/browser/scenarios/box-styled-components/index.js +++ /dev/null @@ -1,27 +0,0 @@ -import * as React from 'react'; -import { createMuiTheme } from '@material-ui/core/styles'; -import { styleFunction } from '@material-ui/core/Box'; -import styledComponents, { - ThemeProvider as StyledComponentsThemeProvider, -} from 'styled-components'; - -const materialSystemTheme = createMuiTheme(); -const BoxStyleComponents = styledComponents('div')(styleFunction); - -export default function BoxStyledComponents() { - return ( - - {new Array(1000).fill().map(() => ( - - styled-components - - ))} - - ); -} diff --git a/benchmark/browser/scenarios/components/index.js b/benchmark/browser/scenarios/components/index.js new file mode 100644 index 00000000000000..887bfb0971a875 --- /dev/null +++ b/benchmark/browser/scenarios/components/index.js @@ -0,0 +1,15 @@ +import * as React from 'react'; + +const Div = React.forwardRef(function Div(props, ref) { + return
; +}); + +export default function Components() { + return ( + + {new Array(1000).fill().map(() => ( +
test case
+ ))} +
+ ); +} diff --git a/benchmark/browser/scenarios/make-styles/index.js b/benchmark/browser/scenarios/make-styles/index.js new file mode 100644 index 00000000000000..81173db4b3d970 --- /dev/null +++ b/benchmark/browser/scenarios/make-styles/index.js @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; + +const useStyles = makeStyles((theme) => ({ + root: { + width: 200, + height: 200, + borderWidth: 3, + borderColor: 'white', + ':hover': { + backgroundColor: theme.palette.secondary.dark, + }, + [theme.breakpoints.up('sm')]: { + backgroundColor: theme.palette.primary.main, + borderStyle: 'dashed', + }, + }, +})); + +const Div = React.forwardRef(function Div(props, ref) { + const classes = useStyles(); + + return
; +}); + +export default function MakeStyles() { + return ( + + {new Array(1000).fill().map(() => ( +
test case
+ ))} +
+ ); +} diff --git a/benchmark/browser/scenarios/primitives/index.js b/benchmark/browser/scenarios/primitives/index.js new file mode 100644 index 00000000000000..03fcff93b9274b --- /dev/null +++ b/benchmark/browser/scenarios/primitives/index.js @@ -0,0 +1,11 @@ +import * as React from 'react'; + +export default function Primitives() { + return ( + + {new Array(1000).fill().map(() => ( +
test case
+ ))} +
+ ); +} diff --git a/benchmark/browser/scenarios/styled-emotion/index.js b/benchmark/browser/scenarios/styled-emotion/index.js new file mode 100644 index 00000000000000..0edabb3f598958 --- /dev/null +++ b/benchmark/browser/scenarios/styled-emotion/index.js @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { createMuiTheme } from '@material-ui/core/styles'; +import emotionStyled from '@emotion/styled'; + +const Div = emotionStyled('div')( + ({ theme }) => ` + width: 200px; + height: 200px; + border-width: 3px; + border-color: white; + :hover { + background-color: ${theme.palette.secondary.dark}; + } + ${[theme.breakpoints.up('sm')]} { + background-color: ${theme.palette.primary.main}; + border-style: 'dashed'; + } +`, +); + +const theme = createMuiTheme(); + +export default function StyledEmotion() { + return ( + + {new Array(1000).fill().map(() => ( +
test case
+ ))} +
+ ); +} diff --git a/benchmark/browser/scenarios/styled-material-ui/index.js b/benchmark/browser/scenarios/styled-material-ui/index.js new file mode 100644 index 00000000000000..18849ead2d3861 --- /dev/null +++ b/benchmark/browser/scenarios/styled-material-ui/index.js @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { experimentalStyled as styled } from '@material-ui/core/styles'; + +const Div = styled('div')( + ({ theme }) => ` + width: 200px; + height: 200px; + border-width: 3px; + border-color: white; + :hover { + background-color: ${theme.palette.secondary.dark}; + } + ${[theme.breakpoints.up('sm')]} { + background-color: ${theme.palette.primary.main}; + border-style: 'dashed'; + } +`, +); + +export default function StyledMaterialUI() { + return ( + + {new Array(1000).fill().map(() => ( +
test case
+ ))} +
+ ); +} diff --git a/benchmark/browser/scenarios/styled-sc/index.js b/benchmark/browser/scenarios/styled-sc/index.js new file mode 100644 index 00000000000000..f81df8ad1317b5 --- /dev/null +++ b/benchmark/browser/scenarios/styled-sc/index.js @@ -0,0 +1,33 @@ +import * as React from 'react'; +import { createMuiTheme } from '@material-ui/core/styles'; +import styledComponents, { + ThemeProvider as StyledComponentsThemeProvider, +} from 'styled-components'; + +const Div = styledComponents('div')( + ({ theme }) => ` + width: 200px; + height: 200px; + border-width: 3px; + border-color: white; + :hover { + background-color: ${theme.palette.secondary.dark}; + } + ${[theme.breakpoints.up('sm')]} { + background-color: ${theme.palette.primary.main}; + border-style: 'dashed'; + } +`, +); + +const theme = createMuiTheme(); + +export default function StyledSC() { + return ( + + {new Array(1000).fill().map(() => ( +
test case
+ ))} +
+ ); +} diff --git a/benchmark/browser/scenarios/sx-prop-box-material-ui/index.js b/benchmark/browser/scenarios/sx-prop-box-material-ui/index.js index 1988668b132ec3..aa5e393dc627ed 100644 --- a/benchmark/browser/scenarios/sx-prop-box-material-ui/index.js +++ b/benchmark/browser/scenarios/sx-prop-box-material-ui/index.js @@ -1,7 +1,7 @@ import * as React from 'react'; import Box from '@material-ui/core/Box'; -export default function BoxSxPropMaterialUI() { +export default function SxPropBoxMaterialUI() { return ( {new Array(1000).fill().map(() => ( @@ -9,16 +9,16 @@ export default function BoxSxPropMaterialUI() { sx={{ width: 200, height: 200, - backgroundColor: [undefined, 'primary.light', 'primary.main', 'primary.dark'], borderWidth: '3px', borderColor: 'white', - borderStyle: [undefined, 'dashed', 'solid', 'dotted'], + backgroundColor: { sm: 'primary.main' }, + borderStyle: { sm: 'dashed' }, ':hover': { backgroundColor: (theme) => theme.palette.secondary.dark, }, }} > - material-ui + test case ))} diff --git a/benchmark/browser/scenarios/sx-prop-box-theme-ui/index.js b/benchmark/browser/scenarios/sx-prop-box-theme-ui/index.js index 588f1aa6fa0c36..95d329dc7668bb 100644 --- a/benchmark/browser/scenarios/sx-prop-box-theme-ui/index.js +++ b/benchmark/browser/scenarios/sx-prop-box-theme-ui/index.js @@ -32,7 +32,7 @@ export default function ThemeUISxPropBox() { }, }} > - theme-ui + test case ))} diff --git a/benchmark/browser/scenarios/sx-prop-div-theme-ui/index.js b/benchmark/browser/scenarios/sx-prop-div-theme-ui/index.js index 38e9abb777c964..e9d33a4d2e8704 100644 --- a/benchmark/browser/scenarios/sx-prop-div-theme-ui/index.js +++ b/benchmark/browser/scenarios/sx-prop-div-theme-ui/index.js @@ -32,7 +32,7 @@ export default function ThemeUiSxProp() { }, }} > - theme-ui + test case
))} diff --git a/benchmark/browser/scripts/benchmark.js b/benchmark/browser/scripts/benchmark.js index b4b9ed32bda7f5..6490072ee52bd6 100644 --- a/benchmark/browser/scripts/benchmark.js +++ b/benchmark/browser/scripts/benchmark.js @@ -56,22 +56,26 @@ const getMedian = (measures) => { }; const printMeasure = (name, measures) => { - console.log(`\n${name}:\n`); + console.log(`${name}:`); let sum = 0; const totalNum = measures.length; measures.forEach((measure) => { sum += measure; - console.log(`${measure.toFixed(2)}ms`); + // Uncomment for more details + // console.log(`${measure.toFixed(2)}ms`); }); + console.log( + `mean: ${Number(sum / totalNum).toFixed(2)}ms, median: ${Number(getMedian(measures)).toFixed( + 2, + )}ms`, + ); console.log('-------------'); - console.log(`Avg: ${Number(sum / totalNum).toFixed(2)}ms`); - console.log(`Median: ${Number(getMedian(measures)).toFixed(2)}ms`); }; -async function runMeasures(browser, testCaseName, testCase, times) { +async function runMeasures(browser, testCaseName, testCase, times = 10) { const measures = []; for (let i = 0; i < times; i += 1) { @@ -95,32 +99,36 @@ async function run() { const [server, browser] = await Promise.all([createServer({ port: PORT }), createBrowser()]); try { - await runMeasures(browser, 'noop (baseline)', './noop/index.js', 10); + // Test that there no significant offset + await runMeasures(browser, 'noop (baseline)', './noop/index.js'); + // Test the cost of React primitives + await runMeasures(browser, 'React primitives', './primitives/index.js'); + // Test the cost of React components abstraction + await runMeasures(browser, 'React components', './components/index.js'); + // Test that @material-ui/styled-engine doesn't add an signifiant overhead + await runMeasures(browser, 'Styled Material-UI', './styled-material-ui/index.js'); + await runMeasures(browser, 'Styled emotion', './styled-emotion/index.js'); + await runMeasures(browser, 'Styled SC', './styled-sc/index.js'); + // Test the performance compared to the v4 standard + await runMeasures(browser, 'makeStyles', './make-styles/index.js'); + // Test that the sx prop vs props spreaing has no signficiant difference + await runMeasures(browser, 'sx Material-UI box', './sx-prop-box-material-ui/index.js'); + await runMeasures(browser, 'Box Material-UI', './box-material-ui-styles/index.js'); + // Test the Box perf with alternatives + await runMeasures(browser, 'sx Theme-UI box', './sx-prop-box-theme-ui/index.js'); + await runMeasures(browser, 'sx Theme-UI div', './sx-prop-div-theme-ui/index.js'); + await runMeasures(browser, 'Box Chakra-UI', './box-chakra-ui/index.js'); + // Test the system perf difference with alternatives await runMeasures( browser, 'styled-components Box + @material-ui/system', './styled-components-box-material-ui-system/index.js', - 10, ); await runMeasures( browser, 'styled-components Box + styled-system', './styled-components-box-styled-system/index.js', - 10, ); - await runMeasures(browser, 'Box emotion', './box-emotion/index.js', 10); - await runMeasures(browser, 'Box @material-ui/styles', './box-material-ui-styles/index.js', 10); - await runMeasures(browser, 'Box styled-components', './box-styled-components/index.js', 10); - await runMeasures( - browser, - 'Basic styled-components box', - './basic-styled-components/index.js', - 10, - ); - await runMeasures(browser, 'Chakra-UI box component', './box-chakra-ui/index.js', 10); - await runMeasures(browser, 'Theme-UI box sx prop', './sx-prop-box-theme-ui/index.js', 10); - await runMeasures(browser, 'Theme-UI div sx prop', './sx-prop-div-theme-ui/index.js', 10); - await runMeasures(browser, 'Material-UI box sx prop', './sx-prop-box-material-ui/index.js', 10); } finally { await Promise.all([browser.close(), server.close()]); } diff --git a/docs/src/pages/system/basics/basics.md b/docs/src/pages/system/basics/basics.md index 7c21300d257b82..6c3de1f29ce87a 100644 --- a/docs/src/pages/system/basics/basics.md +++ b/docs/src/pages/system/basics/basics.md @@ -142,9 +142,34 @@ This prop provides a superset of CSS that maps values directly from the theme, d ### When to use it? -The styled-components's API is great to build low-level components that need to support a wide variety of contexts. +- **styled-components**: the API is great to build components that need to support a wide variety of contexts. These components are used in many different parts of the application and support different combinations of props. +- **`sx` prop**: the API is great to apply one-off styles. It's called "utility" for this reason. -The `sx` prop is great anytime one-off styles need to be applied. It's called "utility" for this reason. +### Performance tradeoff + +The system relies on CSS-in-JS. It works with both emotion and styled-components. + +Pros: + +- 📚 It allows a lot of flexibility in the API. The `sx` prop supports a superset of CSS. There is **no need to learn CSS twice**. You are set once you have learn the standardized CSS syntax, it's safe, it hasn't changed for a decade. Then, you can **optionally** learn the shorthands if you value the save of time they bring. +- 📦 No need for purge. Only the used CSS on the page is included. The initial bundle size cost is **fixed**. It's not growing with the number of used CSS properties. + You pay the cost of [@emotion/react](https://bundlephobia.com/result?p=@emotion/react) and [@material-ui/system](https://bundlephobia.com/result?p=@material-ui/system). It cost around ~15 kB gzipped. + If you are already using the core components, it comes with no extra overhead. + +Cons: + +- The runtime performance take a hit. + + | Benchmark case | Code snippet | Time normalized | + | :-------------------------------- | :------------------- | --------------- | + | a. Render 1,000 primitives | `
` | 100ms | + | b. Render 1,000 components | `
` | 110ms | + | c. Render 1,000 styled components | `` | 160ms | + | d. Render 1,000 Box | `` | 270ms | + + _Head to the [benchmark folder](https://github.com/mui-org/material-ui/tree/next/benchmark/browser) for a reproduction of these metrics._ + + We believe that for most applications, it's **fast enough**. There are simple workarounds when performance becomes critical. For instance, when rendering a list with many items, you can use a CSS child selector to have a single "style injection" point (using d. for the wrapper and a. for each item). ## Usage @@ -315,6 +340,7 @@ All core Material-UI components will support the `sx` prop. ### 2. Box [`Box`](/components/box/) is a lightweight component that gives access to the `sx` prop, and can be used as a utility component, and as a wrapper for other components. +It renders a `
` element by default. ### 3. Custom components