Skip to content
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

Optimize BlockStyles by using hooks and React.memo (instead of HOCs) #21973

Merged
merged 2 commits into from
Apr 30, 2020

Conversation

adamziel
Copy link
Contributor

@adamziel adamziel commented Apr 29, 2020

Description

BlockStyles depend on the value of a block, which means they're re-rendered every time an attribute is updated. This is pretty slow, especially if it happens after every key stroke (see #21948). This PR leverages React.memo and useSelect to avoid re-renders in cases where block example is available.

Test plan

  • To test the case where example is available, create a navigation block.
  • Select your new block, open block inspector, change style a few times, change background color, confirm the styles are rendered correctly.
  • To test the case where example is available, manually remove the example from packages/block-library/src/navigation/index.js and repeat previous steps. I think all core blocks come with an example so it seems to be the easiest way to test. In this scenario, confirm that the style preview is updated to reflect all the updates to block attributes - e.g. changing background color should be reflected in the preview right away.

Types of changes

Bug fix (non-breaking change which fixes an issue)

Checklist:

  • My code is tested.
  • My code follows the WordPress code style.
  • My code follows the accessibility standards.
  • My code has proper inline documentation.
  • I've included developer documentation if appropriate.
  • I've updated all React Native files affected by any refactorings/renamings in this PR.

@adamziel adamziel added [Feature] Inspector Controls The interface showing block settings and the controls available for each block [Type] Performance Related to performance efforts [Feature] Theme Style Variations Related to style variations provided by block themes labels Apr 29, 2020
@adamziel adamziel self-assigned this Apr 29, 2020
@@ -7,6 +7,8 @@ import classnames from 'classnames';
/**
* WordPress dependencies
*/
import isShallowEqual from '@wordpress/is-shallow-equal';
import { memo } from '@wordpress/element';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noting that we also have a "pure" hoc which is the same thing (before the existence of memo), not sure if we should deprecate it or not.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also find both these hocs to be less and less useful with hooks

Copy link
Contributor Author

@adamziel adamziel Apr 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no strong preference here - I'm fine with just memo, but if you think it would be cleaner with pure I'll update it.

@github-actions
Copy link

github-actions bot commented Apr 29, 2020

Size Change: +49 B (0%)

Total Size: 816 kB

Filename Size Change
build/block-editor/index.js 106 kB +44 B (0%)
build/components/index.js 179 kB +5 B (0%)
ℹ️ View Unchanged
Filename Size Change
build/a11y/index.js 1.02 kB 0 B
build/annotations/index.js 3.62 kB 0 B
build/api-fetch/index.js 4.08 kB 0 B
build/autop/index.js 2.82 kB 0 B
build/blob/index.js 620 B 0 B
build/block-directory/index.js 6.23 kB 0 B
build/block-directory/style-rtl.css 760 B 0 B
build/block-directory/style.css 761 B 0 B
build/block-editor/style-rtl.css 10.2 kB 0 B
build/block-editor/style.css 10.2 kB 0 B
build/block-library/editor-rtl.css 7.03 kB 0 B
build/block-library/editor.css 7.03 kB 0 B
build/block-library/index.js 114 kB 0 B
build/block-library/style-rtl.css 7.14 kB 0 B
build/block-library/style.css 7.14 kB 0 B
build/block-library/theme-rtl.css 683 B 0 B
build/block-library/theme.css 685 B 0 B
build/block-serialization-default-parser/index.js 1.88 kB 0 B
build/block-serialization-spec-parser/index.js 3.1 kB 0 B
build/blocks/index.js 48.1 kB 0 B
build/components/style-rtl.css 16.9 kB 0 B
build/components/style.css 16.9 kB 0 B
build/compose/index.js 6.66 kB 0 B
build/core-data/index.js 11.4 kB 0 B
build/data-controls/index.js 1.29 kB 0 B
build/data/index.js 8.44 kB 0 B
build/date/index.js 5.47 kB 0 B
build/deprecated/index.js 772 B 0 B
build/dom-ready/index.js 568 B 0 B
build/dom/index.js 3.1 kB 0 B
build/edit-navigation/index.js 3.54 kB 0 B
build/edit-navigation/style-rtl.css 485 B 0 B
build/edit-navigation/style.css 485 B 0 B
build/edit-post/index.js 27.6 kB 0 B
build/edit-post/style-rtl.css 12.2 kB 0 B
build/edit-post/style.css 12.2 kB 0 B
build/edit-site/index.js 10.9 kB 0 B
build/edit-site/style-rtl.css 5.11 kB 0 B
build/edit-site/style.css 5.11 kB 0 B
build/edit-widgets/index.js 7.5 kB 0 B
build/edit-widgets/style-rtl.css 4.67 kB 0 B
build/edit-widgets/style.css 4.66 kB 0 B
build/editor/editor-styles-rtl.css 428 B 0 B
build/editor/editor-styles.css 431 B 0 B
build/editor/index.js 43.6 kB 0 B
build/editor/style-rtl.css 3.27 kB 0 B
build/editor/style.css 3.27 kB 0 B
build/element/index.js 4.65 kB 0 B
build/escape-html/index.js 733 B 0 B
build/format-library/index.js 7.63 kB 0 B
build/format-library/style-rtl.css 502 B 0 B
build/format-library/style.css 502 B 0 B
build/hooks/index.js 2.13 kB 0 B
build/html-entities/index.js 622 B 0 B
build/i18n/index.js 3.56 kB 0 B
build/is-shallow-equal/index.js 710 B 0 B
build/keyboard-shortcuts/index.js 2.51 kB 0 B
build/keycodes/index.js 1.94 kB 0 B
build/list-reusable-blocks/index.js 3.12 kB 0 B
build/list-reusable-blocks/style-rtl.css 226 B 0 B
build/list-reusable-blocks/style.css 226 B 0 B
build/media-utils/index.js 5.29 kB 0 B
build/notices/index.js 1.79 kB 0 B
build/nux/index.js 3.4 kB 0 B
build/nux/style-rtl.css 616 B 0 B
build/nux/style.css 613 B 0 B
build/plugins/index.js 2.67 kB 0 B
build/primitives/index.js 1.5 kB 0 B
build/priority-queue/index.js 789 B 0 B
build/redux-routine/index.js 2.85 kB 0 B
build/rich-text/index.js 14.8 kB 0 B
build/server-side-render/index.js 2.68 kB 0 B
build/shortcode/index.js 1.7 kB 0 B
build/token-list/index.js 1.28 kB 0 B
build/url/index.js 4.02 kB 0 B
build/viewport/index.js 1.84 kB 0 B
build/warning/index.js 1.14 kB 0 B
build/wordcount/index.js 1.18 kB 0 B

compressed-size-action

Copy link
Contributor

@youknowriad youknowriad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have trouble understanding why we need "memo" here since for me withSelect/useSelect should already not rerender if you don't include the "block" prop (with the updated code)?

@youknowriad
Copy link
Contributor

Some random questions :)

  • how did you test performance impact?
  • did you consider rewriting with useSelect/useDispatch?

@adamziel
Copy link
Contributor Author

adamziel commented Apr 29, 2020

I have trouble understanding why we need "memo" here since for me withSelect/useSelect should already not rerender if you don't include the "block" prop (with the updated code)?

I believe withSelect returns a pure component, but does not make WrappedComponent pure. If parent component passes the same props, React will not re-render. If the callback returns the same props, React will still re-render:

const withSelect = ( mapSelectToProps ) =>
createHigherOrderComponent(
( WrappedComponent ) =>
pure( ( ownProps ) => {
const mapSelect = ( select, registry ) =>
mapSelectToProps( select, ownProps, registry );
const mergeProps = useSelect( mapSelect );
return <WrappedComponent { ...ownProps } { ...mergeProps } />;
} ),
'withSelect'
);

how did you test performance impact?

Nothing overly scientific :-) During my work on #21948 I noticed typing in block inspector is super slow. 60 FPS video would be much better medium to show the problem, but let's try with GIF:

2020-04-29 16-00-14 2020-04-29 16_00_28
2020-04-29 16-01-01 2020-04-29 16_01_24

Without the PR the slowness is impossible to ignore - letters are not showing immediately in response to my input, and they often appear in chunks of 2-6.

did you consider rewriting with useSelect/useDispatch?

I did not! Hmm.. useSelect would receive new block after every key stroke too so it sounds like this approach would still involve memo/pure at some point?

@youknowriad
Copy link
Contributor

I believe withSelect returns a pure component, but does not make WrappedComponent pure. If parent component passes the same props, React will not re-render. If the callback returns the same props, React will still re-render:

🤔 withSelect uses useSelect which IIRC doesn't rerender if the callback returns the same props (at least it's supposed to not rerender)

@adamziel
Copy link
Contributor Author

🤔 withSelect uses useSelect which IIRC doesn't rerender if the callback returns the same props (at least it's supposed to not rerender)

I tried removing the memo() and keeping the other changes in tact and it did the trick, nice - thank you! I'll play with it a bit to leverage useSelect/useDispatch instead of HOCs.

@adamziel
Copy link
Contributor Author

@youknowriad I replaced HOC with hooks (useSelect/useDispatch). In the process, we lost the pure() call from withSelect so I ended up wrapping the export in memo() anyway.

@youknowriad
Copy link
Contributor

Thanks for the updates @adamziel

Do you happen to know why the wrapping in "memo" is needed here? I mean the issue could probably be on a parent component rerendering where it shouldn't?

Copy link
Contributor

@youknowriad youknowriad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code looks good to me, Ideally we get rid of the "memo" usage too if possible.

@adamziel
Copy link
Contributor Author

adamziel commented Apr 30, 2020

@youknowriad that's exactly the case. I would consider it a related, but separate problem. I am profiling right now and if the fix is simple enough I may be able to propose another PR today. I still think we should use memo() here - it costs almost nothing and provides a nice boundary as in "regardless of what happened in the entire tree above, this costly component won't re-render unnecessarily". Without memo(), it would be easy to change something in a seemingly unrelated place and unknowingly break performance in here.

@adamziel
Copy link
Contributor Author

Or am I missing something? Is there any downside to using memo() as long as we're careful not to use it everywhere?

@youknowriad
Copy link
Contributor

@adamziel With the number of components rendered in the tree, memoization has costs, if it's on a component that is rendered very high in the tree, it has almost no cost but the more we go down, the more costful it can be because the same component can be rendered in a loop or something. I think also memoization can hide some problems that need solving so I personally prefer avoiding it if possible.

@adamziel
Copy link
Contributor Author

@youknowriad In principle I agree. In this very specific instance, I still think memo() is justified. This component is indeed pretty deep in the tree, but it's an expensive one - it creates a pretty large, dynamic/arbitrary tree of it's own. It seems unlikely to become a basic component that's instantiated in hundreds/thousands times within a single tree.

As for hiding other problems, I fully agree. One such problem I've seen is that these style preview are full of RichText instances and other edit-specific components. It's not a terrible problem, but ideally they would only contain bare minimum amount of components necessary to display the preview.

Also, I think I found the reason behind the parent component being re-rendered. Let's continue the discussion in #21990

@adamziel adamziel changed the title Use React.memo() in BlockStyles Optimize BlockStyles by using hooks and React.memo (instead of HOCs) Apr 30, 2020
@adamziel adamziel merged commit daa2154 into master Apr 30, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Inspector Controls The interface showing block settings and the controls available for each block [Feature] Theme Style Variations Related to style variations provided by block themes [Type] Performance Related to performance efforts
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants