react-jss: Performance degradation with dynamic values
See original GitHub issueHi, I’m building components library with react-jss and when my stylesheets got large and complex I’ve started to experience performance degradation with dynamic values.
Demo
- Go to examples page
- Enter any values at any description input (you need to type fast to see performance issue)
- See lag in input value
Source code for demo component
Additional example
Video showcase with another component with reproduction instructions
Explanation
I think that this issue is caused by this effect. useStyles
accept data
object which is used as dependency in useLayoutEffect
. If you use styles something like this (similar to docs examples):
useStyles({ size: 'sm', color: 'red' });
Then in createUseStyles useEffect it will resolve in:
useEffectOrLayoutEffect(() => {
if (sheet && dynamicRules && !isFirstMount.current) {
updateDynamicRules(data, sheet, dynamicRules);
}
}, [{ size: 'sm', color: 'red' }]);
Which means that updateDynamicRules
function will be called at each render, this is not noticeable with small amount of styles but degrades performance significantly with larger stylesheets.
My solution
I’ve created a drop in replacement for createUseStyles function that updates props object based on dependencies (it is specific to my project):
function createMemoStyles(styles) {
const useStyles = createUseStyles(styles);
return function useMemoStyles(props) {
const dependencies =
typeof props === 'object' && props !== null
? Object.keys(props)
.filter((key) => key !== 'theme')
.map((key) => props[key])
: [];
if (typeof props === 'object' && 'theme' in props) {
dependencies.push(props.theme.colorScheme);
}
const stylesProps = useMemo(() => props, dependencies);
return useStyles(stylesProps);
};
}
This replacement resolves all performance issues for me.
Possible solution
Calculate effect dependencies based on data in useStyles
:
function useStyles(data: any, dependencies: any[] = []) {
const dependencies =
typeof data === 'object' && data !== null
? Object.keys(data)
.filter((key) => key !== 'theme')
.map((key) => data[key])
: [data];
useEffectOrLayoutEffect(() => {
// We only need to update the rules on a subsequent update and not in the first mount
if (sheet && dynamicRules && !isFirstMount.current) {
updateDynamicRules(data, sheet, dynamicRules);
}
}, [dependencies, ...dependencies]);
}
This will solve issues with primitive values ({ size: 'sm', color: 'red' }
from example above) and will allow to subscribe to effect with any extra values. Theme is filtered as it is always an object and will trigger effect on each render.
Issue Analytics
- State:
- Created 2 years ago
- Reactions:2
- Comments:5 (2 by maintainers)
Top GitHub Comments
Any updates on this issue? Or any recommended approaches?
@rtivital, the code from your example has two issues:
The biggest one is that it only reacts to changes to the values of the keys but not the keys themselves. So while this assumption/statement can be made about your use/library, this isn’t feasible for public implementation.
While this code looks weird and is not great by any means, it still needs to be supported by us.
The code also only works for “flat” objects; e.g., if you have an object as the value in your data object, you would still have the same issue.
Secondly, the
useStyles
hook behaves the same way as any otherReact
hook as the dependencies are not checked for deep equality but rather reference equality.In my opinion, the proper usage in such a case should be to wrap the data object with a
React.useMemo()
on the caller side rather than the library.You could then also think about writing a small utility hook that does this for you based on the dynamic object, e.g.: