Skip to content
This repository has been archived by the owner on Mar 13, 2024. It is now read-only.

MM-35509 Do not use getDerivedStateFromProps to determine picker placement and instead use memoization #8092

Merged
merged 3 commits into from
May 11, 2021

Conversation

jwilander
Copy link
Member

Summary

A customer is experiencing slow typing issues. A JS profile showed the following:

Screen Shot 2021-05-10 at 9 18 51 PM

A very significant amount of time is spent on "Recalculate Style" and the primary trigger for this seems to be the getBoundingClientRect function calls inside the EmojiPickerOverlay component. After some brief investigation it was apparent that on every single character typed into the textbox getBoundingClientRect was being called twice due to improper use of getDerivedStateFromProps. I have used memoization to properly implement the functionality in a much more performant way.

Unfortunately, I was unable to reproduce the exact slow typing issue the customer is experiencing so I'm not sure how much effect this will have or if this is the root cause of the issues or not.

Ticket Link

https://mattermost.atlassian.net/browse/MM-35509

Release Note

Performance improvement to emoji picker overlay that should improve typing performance.

@jwilander jwilander added 2: Dev Review Requires review by a core commiter 3: QA Review Requires review by a QA tester labels May 11, 2021
Copy link
Contributor

@Willyfrog Willyfrog left a comment

Choose a reason for hiding this comment

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

I'm not sure this would help, if function getBoundingRect is the culprit and is being called twice with the same target, wouldn't it be better to wrap and only memoize target.getBoundingRect?
if you memoize the parent call you'd still be calling getBoundingRect twice as they are two different functions.

so something like this might be enough to alleviate the double call:

const getTargetRect = memoize((target) => target.getBoundingRect());

my only concern with that approach is that I'm not entirely sure what target is but most likely an object, if that's the case we'll need to do a custom equality function based on the object to figure out when two call should yield the same result.

edit: digging a bit deeper, seems the overlay is called with a function that returns a reactRef which I think it uses the htmldivelement type. If so we can probably use isSameNode or isEqualNode to do a proper comparison.

edit 2: at least one of the calls is still using a stringRef instead of createRef, since we havent yet finished the migration campaign, so in order for this to work we would need to migrate edit_post_modal.jsx as well

components/emoji_picker/emoji_picker_overlay.jsx Outdated Show resolved Hide resolved
components/emoji_picker/emoji_picker_overlay.jsx Outdated Show resolved Hide resolved
@jwilander
Copy link
Member Author

Thanks for the feedback @Willyfrog

I'm not sure this would help, if function getBoundingRect is the culprit and is being called twice with the same target, wouldn't it be better to wrap and only memoize target.getBoundingRect?
if you memoize the parent call you'd still be calling getBoundingRect twice as they are two different functions.

This helps because before this change on every single character typed getDerivedStateFromProps was running multiple times, even though the render function of EmojiPickerOverlay was not running at all. That's because the parent of EmojiPickerOverlay was rendering (e.g. CreatePost) and that triggers getDerivedStateFromProps even if the props didn't change and no render occurs. So getBoundingClientRect would be called multiple times for every key press. With this change it's only called on render and if the props of EmojiPickerOverlay change. Even if we removed the memoization it would still prevent getBoundingClientRect being called on every key press.

I'm not too worried about the double call when we do actually render, though your code would be a further improvement to prevent that.

getBoundingClientRect is a problem because it can trigger the browser to recalculate style and check the layout. It seems to be doing that way more often for the customer than my local or community testing.

my only concern with that approach is that I'm not entirely sure what target is but most likely an object, if that's the case we'll need to do a custom equality function based on the object to figure out when two call should yield the same result.

edit: digging a bit deeper, seems the overlay is called with a function that returns a reactRef which I think it uses the htmldivelement type. If so we can probably use isSameNode or isEqualNode to do a proper comparison.

edit 2: at least one of the calls is still using a stringRef instead of createRef, since we havent yet finished the migration campaign, so in order for this to work we would need to migrate edit_post_modal.jsx as well

Are we sure a strict comparison is not enough? If the React node is re-created and the ref points to a new node does the value not change? Can that even happen in this case without the EmojiPickerOverlay also being re-created?

@jwilander
Copy link
Member Author

@jgilliam17 for testing we just need to make sure that all the places the Emoji Picker can open, it does so the same as it did before (e.g. which placement it has).

@jgilliam17 jgilliam17 added the Setup Cloud Test Server Setup a test server using Mattermost Cloud label May 11, 2021
@Willyfrog
Copy link
Contributor

This helps because before this change on every single character typed getDerivedStateFromProps was running multiple times, even though the render function of EmojiPickerOverlay was not running at all. That's because the parent of EmojiPickerOverlay was rendering (e.g. CreatePost) and that triggers getDerivedStateFromProps even if the props didn't change and no render occurs. So getBoundingClientRect would be called multiple times for every key press. With this change it's only called on render and if the props of EmojiPickerOverlay change. Even if we removed the memoization it would still prevent getBoundingClientRect being called on every key press.

I agree with this, I was mostly answering on your initial comment about it being called multiple times, but I didn't check where it was being called 👍

Are we sure a strict comparison is not enough? If the React node is re-created and the ref points to a new node does the value not change? Can that even happen in this case without the EmojiPickerOverlay also being re-created?

A strict comparison would also work (it won't work with the default comparison from memoize as the author says it is a shallow one), but if the object already provides a comparison method I usually prefer to trust the one who created object as a better/more efficient way.

anyways, I'll aprove the change 😄

@jwilander jwilander added this to the v5.33.0 milestone May 11, 2021
@jwilander jwilander added the CherryPick/Approved Meant for the quality or patch release tracked in the milestone label May 11, 2021
Copy link
Member

@lieut-data lieut-data left a comment

Choose a reason for hiding this comment

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

Nice analysis, @jwilander! Seems like as solid change, even if we can't tie it directly to the performance issues.

Copy link
Contributor

@jgilliam17 jgilliam17 left a comment

Choose a reason for hiding this comment

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

Thanks @jwilander
Tested, looks good to merge.

  • Verified Emoji Picker placement when opening in main channel text box, edit post, on RHS etc.

@jgilliam17 jgilliam17 removed 2: Dev Review Requires review by a core commiter 3: QA Review Requires review by a QA tester Setup Cloud Test Server Setup a test server using Mattermost Cloud labels May 11, 2021
@mm-cloud-bot
Copy link

Test server destroyed

@jwilander jwilander merged commit 692f907 into master May 11, 2021
@mattermod
Copy link
Contributor

Cherry pick is scheduled.

@jwilander jwilander deleted the emoji-picker-overlay-perf branch May 11, 2021 16:30
mattermost-build pushed a commit to mattermost-build/mattermost-webapp that referenced this pull request May 11, 2021
…ement and instead use memoization (mattermost#8092)

* Do not use getDerivedStateFromProps to determine picker placement and instead use memoization

* Update components/emoji_picker/emoji_picker_overlay.jsx

Co-authored-by: Guillermo Vayá <[email protected]>

* Lint fixes

Co-authored-by: Guillermo Vayá <[email protected]>
(cherry picked from commit 692f907)
@mattermod mattermod added CherryPick/Done Successfully cherry-picked to the quality or patch release tracked in the milestone and removed CherryPick/Approved Meant for the quality or patch release tracked in the milestone labels May 11, 2021
jwilander added a commit that referenced this pull request May 11, 2021
…ement and instead use memoization (#8092) (#8099)

* Do not use getDerivedStateFromProps to determine picker placement and instead use memoization

* Update components/emoji_picker/emoji_picker_overlay.jsx

Co-authored-by: Guillermo Vayá <[email protected]>

* Lint fixes

Co-authored-by: Guillermo Vayá <[email protected]>
(cherry picked from commit 692f907)

Co-authored-by: Joram Wilander <[email protected]>
@amyblais amyblais added Changelog/Done Required changelog entry has been written Docs/Not Needed Does not require documentation and removed Changelog/Done Required changelog entry has been written Docs/Not Needed Does not require documentation labels May 21, 2021
@amyblais amyblais added Changelog/Done Required changelog entry has been written Docs/Not Needed Does not require documentation labels Jun 3, 2021
@amyblais
Copy link
Member

amyblais commented Jun 4, 2021

/cherry-pick release-5.35

@mattermod
Copy link
Contributor

Cherry pick is scheduled.

mattermost-build pushed a commit to mattermost-build/mattermost-webapp that referenced this pull request Jun 4, 2021
…ement and instead use memoization (mattermost#8092)

* Do not use getDerivedStateFromProps to determine picker placement and instead use memoization

* Update components/emoji_picker/emoji_picker_overlay.jsx

Co-authored-by: Guillermo Vayá <[email protected]>

* Lint fixes

Co-authored-by: Guillermo Vayá <[email protected]>
(cherry picked from commit 692f907)
jwilander added a commit that referenced this pull request Jun 4, 2021
…ement and instead use memoization (#8092) (#8212)

* Do not use getDerivedStateFromProps to determine picker placement and instead use memoization

* Update components/emoji_picker/emoji_picker_overlay.jsx

Co-authored-by: Guillermo Vayá <[email protected]>

* Lint fixes

Co-authored-by: Guillermo Vayá <[email protected]>
(cherry picked from commit 692f907)

Co-authored-by: Joram Wilander <[email protected]>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Changelog/Done Required changelog entry has been written CherryPick/Done Successfully cherry-picked to the quality or patch release tracked in the milestone Docs/Not Needed Does not require documentation release-note
Projects
None yet
7 participants