-
Notifications
You must be signed in to change notification settings - Fork 2.9k
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 memoization of LHN by handling the isFocused prop better #28615
Improve memoization of LHN by handling the isFocused prop better #28615
Conversation
Hi @muttmuure, here is a draft of partial improvements that I've done recently. I wanted to share it on Slack but it is quite hard to put such document into Slack message :D Let me know what you think about results and overall shape of such document. Thanks! In the meantime I will work on further improvements 👍 |
@kacper-mikolajczak Thanks! I think this looks great! I agree that the second solution leads to more readable code, adding that one wrapper feels a bit odd to me. @kacper-mikolajczak could you summarize these two options and post it to #newdot-performance room? I think it would be great to get opinions of other agency experts and engineers on this and then we can implement this. Thanks! |
@mountiny Yeah definitely! Thanks for the feedback 👍 |
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.
LGTM ! Just a couple comments.
@@ -20,8 +19,8 @@ import CONST from '../../CONST'; | |||
import reportActionPropTypes from '../../pages/home/report/reportActionPropTypes'; | |||
|
|||
const propTypes = { | |||
/** If true will disable ever setting the OptionRowLHN to focused */ | |||
shouldDisableFocusOptions: PropTypes.bool, | |||
/** Wether row should be focused */ |
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.
/** Wether row should be focused */ | |
/** Whether row should be focused */ |
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.
Done ✅
@@ -91,7 +86,6 @@ function OptionRowLHNData({ | |||
const reportID = propsToForward.reportID; | |||
// We only want to pass a boolean to the memoized component, | |||
// instead of a changing number (so we prevent unnecessary re-renders). | |||
const isFocused = !shouldDisableFocusOptions && currentReportID === reportID; |
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.
We can move this comment to OptionRowLHNDataWithFocus
, since we moved the isFocused
there as well.
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.
Done ✅
shouldDisableFocusOptions: false, | ||
}; | ||
|
||
function OptionRowLHNDataWithFocus({currentReportID, shouldDisableFocusOptions, ...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.
Can we add some comments stating what this OptionRowLHNDataWithFocus
does? It would be cool in future to have an understanding of what it does because not everyone will be coming back to the PR to understand.
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.
Done ✅
Thanks for the thorough review @hurali97. ❤️ Let me know if you approve the changes |
Damn, that looks great! |
@mountiny Yeah that does look promising! Wonder what would be a perceived perf gain, as the list is out of the view when the render happens (it is covered by report screen). But hey, at least we reduced the resources consumption a lil bit :D |
Correct, it leaves more resources for other stuff which is always great. Can you test this on super heavy account we provided for callstack? That is more close to real heavy user usage |
Sure @mountiny, testing it right now! |
Here are the results @mountiny. Mobile (iOS)From testing on iOS it seems that the app performance is highly coupled to the amount of chats present. It is no surprise that it is influencing performance but the app should not slow down almost exponentially when amount of chats grows. My first impression is that there are some heavy calculations based on entire data set going on in each item. I am going to investigate it further in some future, as it is out of the scope of the current RP. WebFor web, I was not able to load the app at all - no errors or anything particularly wrong. After inspecting devtools it seems that there are a lot of calculations (probably related to what Matt and Monil discussed on Slack). |
@kacper-mikolajczak This is testing good for me. Are we waiting on something here? If not, let's open the PR! |
Lets gooo |
Reviewer Checklist
Screenshots/VideosWebScreen.Recording.2023-10-08.at.6.36.03.PM.movMobile Web - ChromeScreen.Recording.2023-10-08.at.6.43.33.PM.movMobile Web - SafariScreen.Recording.2023-10-08.at.6.45.33.PM.movDesktopScreen.Recording.2023-10-08.at.6.41.36.PM.moviOSScreen.Recording.2023-10-08.at.6.46.01.PM.movAndroidScreen.Recording.2023-10-08.at.6.44.55.PM.mov |
I think we can simplify this further without using an additional wrapper component. In here, we can just pass in the Creating a consumer specific wrapper component for each of the variable that needs to be memoed is not a good idea in my opinion. Soon, we'll have more cases like this, which will lead to a lot of more component like these popping up in our code. That will ultimately increase the complexity of our code (Not to mention that it will become hard to understand). PS: This came into my mind while filling in the checklist. |
@allroundexperts Thank you! Do you want to raise that here? https://expensify.slack.com/archives/C05LX9D6E07/p1696354046851689 it might be quicker to discuss and more people ot bound the ideas off |
Hi @allroundexperts, thanks for your feedback! 👍 In the current approach, we simply couple the OptionRowLHNData with the tiny wrapper in the form of OptionRowLHNDataWithFocus (naming itself is another topic - I am not sure it's correct). That's the whole abstraction we introduce. In the suggested approach, we would first need to create withCurrentReportFocused HOC, but then we would still need to bound it to the OptionRowLHNData, which will lead us to creating OptionRowLHNDataWithFocus for this purpose. We just simply moved part of the logic (as shouldDisableFocusOptions still needs to be handled inside the wrapper component). I agree that tightly coupling should be less preferred than composition, but the question is how much do we need to have withCurrentReportFocused abstraction as it is potentially just one line of extracted logic that seems kind of specific. Plus, it looks like it's going to be only used in one place and by sticking to YAGNI rule, we can think about leaving it as-is, wdyt? Having that said, I am not as familiar with the codebase as you are, please let me know if I understood it right. Thanks a lot for the discussion 😄 |
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.
As per the Slack discussion, there was an agreement that we should continue using this pattern. As such, the changes are testing good!
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
Performance Comparison Report 📊Significant Changes To Duration
Show details
Meaningless Changes To DurationShow entries
Show details
|
@Expensify/mobile-deployers 📣 Please look into this performance regression as it's a deploy blocker. |
🚀 Deployed to staging by https://github.com/mountiny in version: 1.3.80-0 🚀
|
🚀 Deployed to production by https://github.com/jasperhuangg in version: 1.3.80-3 🚀
|
🚀 Deployed to staging by https://github.com/mountiny in version: 1.3.81-0 🚀
|
🚀 Deployed to production by https://github.com/jasperhuangg in version: 1.3.83-11 🚀
|
2 similar comments
🚀 Deployed to production by https://github.com/jasperhuangg in version: 1.3.83-11 🚀
|
🚀 Deployed to production by https://github.com/jasperhuangg in version: 1.3.83-11 🚀
|
🚀 Deployed to production by https://github.com/jasperhuangg in version: 1.3.83-11 🚀
|
🚀 Deployed to production by https://github.com/francoisl in version: 1.3.84-10 🚀
|
🚀 Deployed to production by https://github.com/francoisl in version: 1.3.84-10 🚀
|
Intro
Transition between chats (reports), seems to be quite laggy. In this document, we try to investigate the issue and come up with possible solution.
Steps to reproduce
Recording
expensify_loaded_chat_transition_web_dev_recording.mov
Investigations
It looks like there is a big overhead in form of rendering LHN’s list of
OptionRowLHNData
components.In the profiling trace we see that every item in the list is re-rendered. Each re-render takes up to 5ms to complete, which with current list size can significantly throttle rendering.
The re-rendering is caused by change in
currentReportID
state, which is provided bywithCurrentReportID
HOC.currentReportID
prop is used inOptionRowLHNData
in one place, to find out wether component should be focused or not:The
isFocused
is then directly passed down into child.The change in
currentReportID
breaks memoization, causing heavy re-render of each item.Solutions
Wrapper component
As
currentReportID
is only used once andisFocused
is a boolean flag, we can extract this logic into separate wrapper component and refactorOptionRowLHNData
to receive primitiveisFocused
prop, which will not break memoization.By applying these changes, we were able to shrink down the rendering time of List component to ~100ms, which will be even more significant as number of list items grows. The rest of rendering overhead seems to be related to different components,
Report
itself etc.The rendering time should be now only merely related to list size, as it only triggers one heavy re-render.
Wrapper component would look more or less like this:
This solution does not add anything beyond a simple wrapper so it is quite easy to do and should not be error-prone.
Use
withCurrentReportID
in list component (Alternative)Alternatively, we can use
withCurrentReportID
once at the level of list component instead of in each children. Then we will directly passisFocused
prop into children, so it won’t break memoization of list items.The solution is more elegant, as it uses
currentReportID
provider once and does not expose such internals into children components. On the flipside, we need to re-render list itself, which might add some overhead.In this scenario, it takes about ~120ms to render the list, which is about 15ms slower than previous solution.
Conclusions
While we managed to reduce rendering times, we still need to think on which solution should be implemented. Furthermore, it might be a good idea to investigate rendering of list items, as it is quite simple component but still takes ~5ms to render.
Fixed Issues
$ #28631
PROPOSAL: #28631 (it contains a proposal)
Tests
Offline tests
N/A
QA Steps
PR Author Checklist
### Fixed Issues
section aboveTests
sectionOffline steps
sectionQA steps
sectiontoggleReport
and notonIconClick
)myBool && <MyComponent />
.src/languages/*
files and using the translation methodWaiting for Copy
label for a copy review on the original GH to get the correct copy.STYLE.md
) were followedAvatar
, I verified the components usingAvatar
are working as expected)/** comment above it */
this
properly so there are no scoping issues (i.e. foronClick={this.submit}
the methodthis.submit
should be bound tothis
in the constructor)this
are necessary to be bound (i.e. avoidthis.submit = this.submit.bind(this);
ifthis.submit
is never passed to a component event handler likeonClick
)StyleUtils.getBackgroundAndBorderStyle(themeColors.componentBG)
)Avatar
is modified, I verified thatAvatar
is working as expected in all cases)ScrollView
component to make it scrollable when more elements are added to the page.main
branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTest
steps.Screenshots/Videos
Web
Screen.Recording.2023-10-04.at.17.06.42.mov
Mobile Web - Chrome
N/A
Mobile Web - Safari
N/A
Desktop
Screen.Recording.2023-10-04.at.17.15.05.mov
iOS
Mobile recording for reference
Screen.Recording.2023-10-04.at.17.05.00.mov
Android
N/A