-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Improve performance; Stop unnecessary rerenders caused by withColors #6686
Improve performance; Stop unnecessary rerenders caused by withColors #6686
Conversation
return setColorValue( colors, colorNameAttribute, customColorAttribute, setAttributes ); | ||
}; | ||
return mapGetSetColorToProps( getColor, setColor, props ); | ||
return mapGetSetColorToProps( memoizedGetColor( colors ), memoizedSetColor( props.setAttributes )( colors ), props ); |
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.
props.setAttributes
- out of curiosity, why would it ever change?
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.
Seems like a straightforward change that makes sense to me.
I guess I share @gziolo's question about why props.setAttributes
would change, but the approach here looks good and causes fewer re-renders 👍
2721e3d
to
e45ac23
Compare
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.
Thanks, looks great!
Could you make a minor change to the comment before merging? I just found the current one a bit hard to parse.
/** | ||
* Even though, we don't expect setAttributes to change memoizing it is essential. | ||
* If it is not memoized, each time memoizedSetColor is called, | ||
* even if setAttributes was the same as before a new function reference is returned. |
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.
Nitpick here but this is hard to parse. Could you modify the sentence a bit so it's easier to follow cause-and-effect, eg:
* If setAttributes is not memoized, each time memoizedSetColor is called:
* a new function reference is returned (even if setAttributes has not changed).
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.
Thank you for the improvements they were applied 👍
The getColor and setColor functions returned a new object or a new function every time, although the code looked correct it made unnecessary rerenders happen. We now returned memoized functions this ensure that if the parameters are not changed we will return references for the same object or function so no rerender happen.
e45ac23
to
56a11cb
Compare
@@ -1,7 +1,7 @@ | |||
/** | |||
* External dependencies | |||
*/ | |||
import { get } from 'lodash'; | |||
import { get, memoize } from 'lodash'; |
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.
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.
Thanks, I've filed #6707 as a follow-up.
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.
* a new function reference is returned (even if setAttributes has not changed). | ||
* This would make our memoized chain useless. | ||
*/ | ||
const memoizedSetColor = memoize( |
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.
3 nested memoize
calls look suspicious in here. Maybe it's okey, just saying :)
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.
Also, why would we want to preserve cache references to old setAttributes
? Seems like a use-case for memoize-one
or a WeakMap
-based cache like the withWeakMapCache
proposed in #6395.
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.
Unfortunately, memoize-one would not fit our use case, as each block has its setAttributes function and we would need to cache it for this approach to work.
I had the idea in mind that when a block was deleted, we would stop referencing the memoized function which would make its cache clear. But unfortunately given that I made the memoize call a top-level function, that is not true. So when blocks are deleted, we are still catching its deleted setAttributes :( Using WeakMap caching it would make the logic correct without leaks I think.
But anyway I got mind bugged and this solution is complex and probably not something we should pursue in the long term.
At the time using memoize seemed the best possibility to solve the rerender problem being back-compatible, now it seems the best long-term solution is changing the API. I'm still a little bit curious about how the rerenders could be solved keeping the same API without a WeakMap (and the preservation of references in deleted blocks) it seems there is a simple way and I'm not seeing it but now it is more an academic curiosity and not something I would pursue in the long term.
Thank you for your reviews I will follow up with them to address this problem.
@@ -14,6 +14,40 @@ import { withSelect } from '@wordpress/data'; | |||
*/ | |||
import { getColorValue, getColorClass, setColorValue } from './utils'; | |||
|
|||
const memoizedGetColor = memoize( | |||
( colors ) => |
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.
There's a potential memory leak here when getEditorSettings().colors
is not defined. In the withSelect
below, you pass []
as the defaultValue
in the get
call. Since [] !== []
, these will be stored as separate entries in the memoization cache.
$ n_
n_ > var fn = () => console.log( 'called' );
undefined
n_ > var memoized = _.memoize( fn );
undefined
n_ > memoized( [] );
called
undefined
n_ > memoized( [] );
called
undefined
n_ > memoized.cache.size
2
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.
Nice catch 👍
Why do we leave it to the block implementer to provide a mapping function? Particularly when I imagine these prop names would never change. Since it all seems to be based around the underlying component receiving a prop of values, I wonder if we could just accept this name as a sole argument of const DEFAULT_COLORS = [];
export const withColors = ( propName ) => createHigherOrderComponent(
( WrappedComponent ) => {
const upperFirstName = upperFirst( propName );
const attributeName = propName;
const customAttributeName = 'custom' + upperFirstName;
const setterPropName = 'set' + upperFirstName;
const contextName = kebabCase( attributeName );
class WithColorsComponent extends Component {
constructor() {
super( ...arguments );
this.setColor = this.setColor.bind( this );
}
shouldComponentUpdate( nextProps ) {
const { attributes } = this.props;
const { attributes: nextAttributes } = nextProps;
return (
attributes[ attributeName ] !== nextAttributes[ attributeName ] ||
attributes[ customAttributeName ] !== nextAttributes[ customAttributeName ]
);
}
setColor( value ) {
const { colors, setAttributes } = this.props;
setColorValue( colors, attributeName, customAttributeName, setAttributes );
}
render() {
const { colors, attributes } = this.props;
const color = attributes[ attributeName ];
const customColor = attributes[ customAttributeName ];
return (
<WrappedComponent
{ ...this.props }
{ ...{
[ attributeName ]: {
name: color,
class: getColorClass( contextName, color ),
value: getColorValue( colors, color, customColor ),
},
[ setterPropName ]: this.setColor,
} }
/>
);
}
}
return withSelect( ( select ) => {
const { getEditorSettings } = select( 'core/editor' );
const colors = get( getEditorSettings(), [ 'colors' ], DEFAULT_COLORS );
return { colors };
} );
},
'withColors'
); Then paragraph block uses as: compose( [
withColors( 'color' ),
withColors( 'backgroundColor' ),
] )( ParagraphBlock ) |
Hi @aduth, thank you for your suggestions.
It only rerenders when textColor changes. I'm probably missing something and complicating things, I will keep thinking about a solution. Edit: |
The getColor and setColor functions returned a new object or a new function every time, although the code looked correct it made unnecessary rerenders happen.
We now return memoized functions this ensures that if the parameters are not changed we will return references for the same object or function so the props the HOC returns are not changed and no rerender happen.
How has this been tested?
Verify setting colors in paragraph and button continues to work the same way as before.
Toggle the Highlights updates feature and verify, and verify the number of rerenders decreased.
Screenshots
Before:
After: