Skip to content

Commit

Permalink
[Box][styled] Cache makeStyles() per JSON of props for style functions
Browse files Browse the repository at this point in the history
  • Loading branch information
ypresto committed Aug 7, 2019
1 parent 78db8a0 commit 0ce6450
Show file tree
Hide file tree
Showing 9 changed files with 286 additions and 42 deletions.
1 change: 1 addition & 0 deletions packages/material-ui-benchmark/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"docs": "cd ../../ && NODE_ENV=production BABEL_ENV=benchmark babel-node packages/material-ui-benchmark/src/docs.js --inspect=0.0.0.0:9229",
"server": "cd ../../ && NODE_ENV=production BABEL_ENV=benchmark babel-node packages/material-ui-benchmark/src/server.js --inspect=0.0.0.0:9229",
"styles": "cd ../../ && NODE_ENV=production BABEL_ENV=benchmark babel-node packages/material-ui-benchmark/src/styles.js --inspect=0.0.0.0:9229",
"box": "cd ../../ && NODE_ENV=production BABEL_ENV=benchmark babel-node packages/material-ui-benchmark/src/box.js --inspect=0.0.0.0:9229",
"system": "cd ../../ && NODE_ENV=production BABEL_ENV=benchmark babel-node packages/material-ui-benchmark/src/system.js --inspect=0.0.0.0:9229",
"test": "exit 0"
},
Expand Down
86 changes: 86 additions & 0 deletions packages/material-ui-benchmark/src/box.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/* eslint-disable no-console */

import Benchmark from 'benchmark';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import { SheetsRegistry } from 'react-jss';
import { makeStyles, StylesProvider, styled } from '@material-ui/styles';
import Box, { styleFunction } from '@material-ui/core/Box';

const suite = new Benchmark.Suite('box', {
onError: event => {
console.log(event.target.error);
},
});
Benchmark.options.minSamples = 100;

const cssObject = {
root: {
padding: 2,
margin: 2,
},
};

const useStyles = makeStyles(cssObject);
function HookBox(props) {
const classes = useStyles();
return <div className={classes.root} {...props} />;
}

const NoCacheBox = styled('div')(styleFunction, { name: 'MuiBox', _useStylesCache: null });

suite
.add('Box', () => {
const sheetsRegistry = new SheetsRegistry();
ReactDOMServer.renderToString(
<StylesProvider sheetsManager={new Map()} sheetsRegistry={sheetsRegistry}>
{Array.from(new Array(100)).map((_, index) => (
<Box key={String(index)} padding={2} margin={2}>
Material-UI
</Box>
))}
</StylesProvider>,
);
sheetsRegistry.toString();
})
.add('Box with disable props cache', () => {
const sheetsRegistry = new SheetsRegistry();
ReactDOMServer.renderToString(
<StylesProvider sheetsManager={new Map()} sheetsRegistry={sheetsRegistry}>
{Array.from(new Array(100)).map((_, index) => (
<NoCacheBox key={String(index)} padding={2} margin={2}>
Material-UI
</NoCacheBox>
))}
</StylesProvider>,
);
sheetsRegistry.toString();
})
.add('useStyles', () => {
const sheetsRegistry = new SheetsRegistry();
ReactDOMServer.renderToString(
<StylesProvider sheetsManager={new Map()} sheetsRegistry={sheetsRegistry}>
{Array.from(new Array(100)).map((_, index) => (
<HookBox key={String(index)}>Material-UI</HookBox>
))}
</StylesProvider>,
);
sheetsRegistry.toString();
})
.add('Inline style', () => {
const sheetsRegistry = new SheetsRegistry();
ReactDOMServer.renderToString(
<StylesProvider sheetsManager={new Map()} sheetsRegistry={sheetsRegistry}>
{Array.from(new Array(100)).map((_, index) => (
<div key={String(index)} style={cssObject.root}>
Material-UI
</div>
))}
</StylesProvider>,
);
sheetsRegistry.toString();
})
.on('cycle', event => {
console.log(String(event.target));
})
.run();
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,19 @@ export default function createGenerateClassName(options = {}) {
[
'Material-UI: you might have a memory leak.',
'The ruleCounter is not supposed to grow that much.',
].join(''),
].join('\n'),
);

const name = styleSheet.options.name;

// Is a global static MUI style?
if (name && name.indexOf('Mui') === 0 && !styleSheet.options.link && !disableGlobal) {
if (
name &&
name.indexOf('Mui') === 0 &&
!styleSheet.options.muiDynamic &&
!styleSheet.options.link &&
!disableGlobal
) {
// We can use a shorthand class name, we never use the keys to style the components.
if (pseudoClasses.indexOf(rule.key) !== -1) {
return `Mui-${rule.key}`;
Expand Down
24 changes: 1 addition & 23 deletions packages/material-ui-styles/src/makeStyles/makeStyles.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { StylesContext } from '../StylesProvider';
import { increment } from './indexCounter';
import getStylesCreator from '../getStylesCreator';
import noopTheme from '../getStylesCreator/noopTheme';
import useSynchronousEffect from './useSynchronousEffect';

function getClasses({ state, stylesOptions }, classes, Component) {
if (stylesOptions.disableGeneration) {
Expand Down Expand Up @@ -160,29 +161,6 @@ function detach({ state, theme, stylesOptions, stylesCreator }) {
}
}

function useSynchronousEffect(func, values) {
const key = React.useRef([]);
let output;

// Store "generation" key. Just returns a new object every time
const currentKey = React.useMemo(() => ({}), values); // eslint-disable-line react-hooks/exhaustive-deps

// "the first render", or "memo dropped the value"
if (key.current !== currentKey) {
key.current = currentKey;
output = func();
}

React.useEffect(
() => () => {
if (output) {
output();
}
},
[currentKey], // eslint-disable-line react-hooks/exhaustive-deps
);
}

function makeStyles(stylesOrCreator, options = {}) {
const {
// alias for classNamePrefix, if provided will listen to theme (required for theme.overrides)
Expand Down
4 changes: 4 additions & 0 deletions packages/material-ui-styles/src/makeStyles/multiKeyStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ const multiKeyStore = {
},
delete: (cache, key1, key2) => {
const subCache = cache.get(key1);
if (!subCache) return;
subCache.delete(key2);
if (subCache.size === 0) {
cache.delete(key1);
}
},
};

Expand Down
24 changes: 24 additions & 0 deletions packages/material-ui-styles/src/makeStyles/useSynchronousEffect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';

export default function useSynchronousEffect(func, values) {
const key = React.useRef([]);
let output;

// Store "generation" key. Just returns a new object every time
const currentKey = React.useMemo(() => ({}), values); // eslint-disable-line react-hooks/exhaustive-deps

// "the first render", or "memo dropped the value"
if (key.current !== currentKey) {
key.current = currentKey;
output = func();
}

React.useEffect(
() => () => {
if (output) {
output();
}
},
[currentKey], // eslint-disable-line react-hooks/exhaustive-deps
);
}
64 changes: 64 additions & 0 deletions packages/material-ui-styles/src/styled/createCachedUseStyles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import makeStyles from '../makeStyles';
import multiKeyStore from '../makeStyles/multiKeyStore';
import useSynchronousEffect from '../makeStyles/useSynchronousEffect';
import useTheme from '../useTheme';
import noopTheme from '../getStylesCreator/noopTheme';
import warning from 'warning';

function pick(input, fields) {
const output = {};

Object.keys(input).forEach(prop => {
if (fields.indexOf(prop) >= 0) {
output[prop] = input[prop];
}
});

return output;
}

export default function createCachedUseStyles({
styleFunction,
filterProps,
makeStylesOptions,
cacheStore,
}) {
return props => {
const theme = useTheme() || noopTheme;
const propsForStyle = pick(props, filterProps);

let hasFunc = false;
const cacheKey = JSON.stringify(propsForStyle, (_key, value) => {
if (typeof value !== 'function') return value;
if (!hasFunc) {
warning(false, 'Material-UI: You can not pass a function as style attribute.');
hasFunc = true;
}
return 'invalid';
});

let cacheEntry = multiKeyStore.get(cacheStore, theme, cacheKey);
if (!cacheEntry) {
cacheEntry = {
useStyles: makeStyles(
{ root: styleFunction({ theme, ...propsForStyle }) },
{ ...makeStylesOptions, muiDynamic: true },
),
refs: 0,
};
if (!hasFunc) multiKeyStore.set(cacheStore, theme, cacheKey, cacheEntry);
}

useSynchronousEffect(() => {
cacheEntry.refs += 1;
return () => {
cacheEntry.refs -= 1;
if (cacheEntry.refs <= 0) {
multiKeyStore.delete(cacheStore, theme, cacheKey);
}
};
}, [cacheEntry, theme, cacheKey]);

return cacheEntry.useStyles(props);
};
}
33 changes: 16 additions & 17 deletions packages/material-ui-styles/src/styled/styled.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import { chainPropTypes, getDisplayName } from '@material-ui/utils';
import hoistNonReactStatics from 'hoist-non-react-statics';
import makeStyles from '../makeStyles';
import createCachedUseStyles from './createCachedUseStyles';

function omit(input, fields) {
const output = {};
Expand All @@ -21,7 +22,7 @@ function omit(input, fields) {
// Using components as a low-level styling construct can be simpler.
function styled(Component) {
const componentCreator = (style, options = {}) => {
const { name, ...stylesOptions } = options;
const { name, _useStylesCache = new Map(), ...stylesOptions } = options;

if (process.env.NODE_ENV !== 'production' && Component === undefined) {
throw new Error(
Expand All @@ -47,27 +48,25 @@ function styled(Component) {
? theme => ({ root: props => style({ theme, ...props }) })
: { root: style };

const useStyles = makeStyles(stylesOrCreator, {
const makeStylesOptions = {
Component,
name: name || Component.displayName,
classNamePrefix,
...stylesOptions,
});

let filterProps;
let propTypes = {};

if (style.filterProps) {
filterProps = style.filterProps;
delete style.filterProps;
}
};

/* eslint-disable react/forbid-foreign-prop-types */
if (style.propTypes) {
propTypes = style.propTypes;
delete style.propTypes;
}
/* eslint-enable react/forbid-foreign-prop-types */
/* eslint-disable-next-line react/forbid-foreign-prop-types */
const { filterProps, propTypes = {} } = style;

const useStyles =
_useStylesCache && filterProps
? createCachedUseStyles({
styleFunction: style,
filterProps,
makeStylesOptions,
cacheStore: _useStylesCache,
})
: makeStyles(stylesOrCreator, makeStylesOptions);

const StyledComponent = React.forwardRef(function StyledComponent(props, ref) {
const {
Expand Down
Loading

0 comments on commit 0ce6450

Please sign in to comment.