-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Optimize the first rendering of the report main view and the report actions view #498
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
Conversation
|
|
||
| // If the batchCallback returned false, then the props to get the items | ||
| // from didn't exist from Ion yet | ||
| if (items === false) { |
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.
This will go away once #482 is merged
marcaaron
left a comment
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 for taking a crack at this @tgolen. It took me a while to wrap my head around how withBatchedRendering works, but I appreciate what you’ve done here so far.
Now, my instinct is that this HOC may point to a deeper problem somewhere else in the code. And I wonder if we really need it at all. However, I haven’t investigated performance issues yet so I won't share any wild guesses right now. And I won't block this solution if it's the glue that gets us from A to B.
I did spot a few problems that we should address and had a few suggestions for how to improve things.
src/page/home/MainView.js
Outdated
| }; | ||
|
|
||
| class MainView extends React.Component { | ||
| // This is a PureComponent because since this method is connected to an Ion collection, |
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.
because since
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.
I think you maybe mean "component" not "method"?
src/page/home/MainView.js
Outdated
| // it has setState() called on it anytime a single report in the collection changes. When | ||
| // reports are first fetched, this component is rendered n times (where n is the number of reports). | ||
| // By switching to a PureComponent, it will only re-render if the props change which is | ||
| // much more performant. |
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.
this component is rendered n times (where n is the number of reports)
Might be reading this wrong, but I think this PR should make this statement no longer true? We will get all the reports then set the entire collection to withIon state before the component renders. So, we should expect it to only render once.
By switching to a PureComponent, it will only re-render if the props change which is much more performant.
Haven't used PureComponent much, but based on what I've read in the docs my understanding is that it just does a shallow comparison for all props and state to see if the component needs to re-render. Lmk if there's some other difference you see. It's still unclear to me why this would be more performant after reading this comment, but I am curious to 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.
Might be reading this wrong, but I think this PR should make this statement no longer true?
I don't think it does, no. I checked out that branch, and I added a console.log('render') to the render method in MainView.js and this is what happens on page load (ignore the network errors):
I then changed that component to be a PureComponent (still on that same branch) and this is what happens:
Lmk if there's some other difference you see
No, there are no other differences, that's exactly what PureComponents do and that's what we want it to do. The only thing this component needs to do is render/show/hide the ReportViews for every report. It doesn't need any information from the reports collection other than the reportID, which is never going to change after first render. The only other thing it uses is this.props.match.params.reportID, and since those props are updated via the router, it will re-render the component when necessary.
It's still unclear to me why this would be more performant after reading this comment, but I am curious to learn more.
It's the difference between rendering all the ReportViews 52 times (literally) or 1 time, so there is a huge performance gain.
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.
It doesn't need any information from the reports collection other than the reportID, which is never going to change after first render
Cool. That explanation helps! Would be good to include something about the reportIDs in the comment.
So if a new report appears in the props (e.g. if Pusher handles a new report comment for a report not yet in your list) then it should appear because it's a new key on this.props.reports, but other updates to existing reports should not trigger the re-render?
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.
Yeah, that's the idea. Let me be sure to check that pusher case 👍
src/page/home/MainView.js
Outdated
| return ( | ||
| <> | ||
| {_.map(this.props.reports, report => ( | ||
| {_.map(this.props.itemsToRender, report => report.reportName && ( |
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.
Just wondering here... are we rendering all these reports because after they are loaded it makes for a better overall performance when switching between them?
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.
Yes, exactly.
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.
Any ideas about why performance degrades when switching reports?
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.
Yea, it's because all the comments have to be inserted into the DOM when reports are switched. Since the process to do DOM insertion is expensive, if we can do it in a way that doesn't impact the user, then it's a better experience.
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.
Makes sense. So, if there are less comments to insert into the DOM (or whatever the equivalent is on mobile) then it would be faster to switch between chats. But we have instead decided to "front-load" that work. And this PR is basically prioritizing that work by delaying some of the "front-loaded" parts.
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.
Yeah, that's correct.
|
|
||
| // Array of report actions for this report | ||
| reportActions: PropTypes.PropTypes.objectOf(PropTypes.shape(ReportActionPropTypes)), | ||
| itemsToRender: PropTypes.PropTypes.arrayOf(PropTypes.shape(ReportActionPropTypes)), |
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.
Could we possibly use a dynamic prop name here? It's an interesting idea to use this HOC as a prop filtering middleware of sorts... but I wonder if we need to lose the prop name as it makes it more difficult to follow the rest of the code.
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.
That's a good idea. I'll look into that to see if I can keep the original prop name intact.
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.
So I looked into this, and I actually don't want to use the original prop name (because that will overwrite the data that Ion passes to the component). I like having a separate prop for what withIon adds and withBatchedRendering adds. I have a little idea to improve this to make it more intuitive.
| return component.displayName || component.name || 'Component'; | ||
| } | ||
|
|
||
| export default function (batchCallback, batchesToRender = 2, batchRenderDelay = 5000) { |
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.
Random thought... what if batchCallback returned an array where each item was a batch that we want to render + a render delay?
e.g.
(props) => {
return [
{items: _.filter(props.something, someFilter), delay: 0},
{items: _.filter(props.something, someOtherFilter), delay: 2000},
];
}I think this makes it slightly easier to visualize the "batching" that's happening and also give us more control.
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.
Yeah, that's a really cool idea. I like how that looks.
|
|
||
| setTimeout(() => { | ||
| this.setState({ | ||
| itemsToRender: items, |
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.
Only update itemsToRender when a batch runs? What about when the props update? Don't we want to continue to set the latest props once all the filtering is done?
src/page/home/MainView.js
Outdated
|
|
||
| class MainView extends React.Component { | ||
| // This is a PureComponent because since this method is connected to an Ion collection, | ||
| // it has setState() called on it anytime a single report in the collection changes. When |
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 are calling setState() on withIon not here, but I got your meaning
This reverts commit 3f77ea9.
|
When you say "first 100 comments" -- can you actually prioritize the "last
100"?
…On Tue, Sep 15, 2020, 6:51 PM Tim Golen ***@***.***> wrote:
This fixes #443 <#443>
I hope. I wasn't able to reproduce the problem locally, so maybe one of you
are able to?
This PR adds an HOC which will allow a component to render a collection in
batches. In the case of the report main view, the batches are based off of
first the visible reports, then 5 seconds later, the rest of the reports.
On the report actions view, the first batch is 100 comments, then 5 seconds
later, the rest of the comments.
Tests
1. From a cold boot of the app, sign in, and search with the chatpicker
2. Verify it is very very fast to do so
------------------------------
You can view, comment on, or merge this pull request online at:
#498
Commit Summary
- Optimize how many reports render at one time
- Add some comments to explain the pure component
- Optimize the rendering of report actions
- Add HOC for batched rendering
File Changes
- *A* src/components/withBatchedRendering.js
<https://github.com/Expensify/ReactNativeChat/pull/498/files#diff-cf2df8cc07d2f2021e82db90331a4ab6>
(69)
- *M* src/page/home/MainView.js
<https://github.com/Expensify/ReactNativeChat/pull/498/files#diff-0ef12fb26b941afa1462ce918baade5f>
(59)
- *M* src/page/home/report/ReportActionsView.js
<https://github.com/Expensify/ReactNativeChat/pull/498/files#diff-c91277fc49b844dae6d2386aee8aa2f7>
(32)
Patch Links:
- https://github.com/Expensify/ReactNativeChat/pull/498.patch
- https://github.com/Expensify/ReactNativeChat/pull/498.diff
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<#498>, or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAEMNUVFDFELANQ4OD3RGU3SF7VVRANCNFSM4RN4JQ3Q>
.
|
…actNativeChat into tgolen-first-render
| _.each(batches(this.props), (batch) => { | ||
| setTimeout(() => { | ||
| this.setState({ | ||
| itemsToRender: batch.items, |
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.
I think we need to listen for changes to whatever prop we are filtering with this HOC in componentDidUpdate() and then set it to state or else we will never get the updated props?
Try leaving a comment on a report. It won't work at all and I think this is the reason why.
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.
Ah, I'll look into that, yeah. I wasn't done with the most recent changes I wanted to make so there are still a few things I haven't tested yet. THanks!
src/page/home/MainView.js
Outdated
| return ( | ||
| <> | ||
| {_.map(this.props.reports, report => ( | ||
| {_.map(this.props.itemsToRender, report => report.reportName && ( |
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.
Makes sense. So, if there are less comments to insert into the DOM (or whatever the equivalent is on mobile) then it would be faster to switch between chats. But we have instead decided to "front-load" that work. And this PR is basically prioritizing that work by delaying some of the "front-loaded" parts.
| const sortedReportActions = _.sortBy(props.reportActions, 'sequenceNumber'); | ||
| return [ | ||
| {items: _.chain(sortedReportActions).last(100).indexBy('reportID').value(), delay: 0}, | ||
| {items: _.indexBy(sortedReportActions, 'reportID'), delay: 7000}, |
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.
sequenceNumber right? not 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.
Yep :D
|
@quinthar RE: the 100 comment, yes, it does prioritize the most recent 100 report actions. |
|
@marcaaron OK, I've updated it so that it works with
There are a couple of solutions I guess.
Thoughts? |
…actNativeChat into tgolen-first-render
|
Ah hmm, yeah one way around that would be to do something like this: return [
{batchMethod: (props) => {...}, delay: 0},
{batchMethod: (props) => {...}, delay: 10000}
];So rather than making batches a function you make each batch have a function that will refer to latest |
|
Gonna take this over for Tim and pick up where we left off. |
…actNativeChat into tgolen-first-render
|
Removing myself as reviewer since I added a few commits to get this cleaned up - @chiragsalian @roryabraham would you all mind taking a look at this? |
| }} | ||
| onChangeText={this.updateSearch} | ||
| onClearButtonClick={this.reset} | ||
| onClearButtonClick={() => this.reset()} |
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.
This was a random bug I found that was passing a Class as a param to this.reset() didn't seem to be breaking anything but was popping up in console.logs for proptypes errors.
| * @param {object} component | ||
| * @returns {string} | ||
| */ | ||
| function getDisplayName(component) { |
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.
IIRC this function is the same for all our HOC's, can we DRY this up and make it a separate utility?
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.
Yeah I was thinking of doing this but just left it since the proposed changes are kind of complicated and I prefer to have fewer file changes per PR if possible.
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.
Alright, it's NAB for me because this function is super simple
| }, | ||
| })(ReportActionsView); | ||
| export default compose( | ||
| withIon({ |
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.
Just wondering, does it matter the order in which these HOC's are composed? I think it's right the way you have it here, but I'm still curious if having withIon be wrapped by withBatchedRendering would break withIon?
Just kinda thinking out loud here, but it seems like withBatchedRendering temporarily withholds certain props from being passed to the wrapped component. If that wrapped component were withIon, I'm wondering if withIon's componentDidMount would still set up all the Ion subscriptions correctly
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.
Yeah the order does matter, but I think even if you have withBatchedRendering followed by withIon that nothing would inherently be wrong with that. We are just not likely to ever do it. withBatchedRendering is a just a dumb middleware that takes props and delays passing them down to the wrapped component. Whether that component is wrapped in withIon or not doesn't shouldn't really make a difference.
I could see the argument that if you used withBatchedRendering in front of a withIon that was using the function version of defining an Ion key like
{key: props => props.someValueThatWithBatchedRenderingIsTemporarilyWitholding}Then maybe this initial subscription wouldn't work out in componentDidMount()
But in that case, it's a possible indication that we are holding something wrong.
Even if that did happen we would retry to subscribe pretty much the next time the component updates...
roryabraham
left a comment
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.
Overall looks good, just the one DRY comment for getDisplayName for now - the other is NAB and more just a question.
| }); | ||
| } | ||
|
|
||
| componentDidUpdate(prevProps) { |
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, if I'm being honest I'm confused about why we need this function and why this.props[propNameToBatch] is a list.
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.
Ok hmm I'm trying to think of a better way to put this...
Right now I have
We need this to allow the flow of props down from a parent component to work normally after all the batches have finished rendering
But maybe the part you're missing is that...
- We are effectively replacing the property coming in from
propswith the one set instatein this HOC - We explicitly remove the property from
this.propsin therender()so the props passed by a parent are filtered through this component - If we don't listen for when that property changes and set it to state we'll just remove it and the wrapped component's props will never update
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.
Okay, I think I get it now
roryabraham
left a comment
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.
Changing this to an approval, but I'll let @chiragsalian have a look too.
chiragsalian
left a comment
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.
Looks great to me and works really well on web and mobile too. Left two minor comments. Approved anyway since they are mostly NABs.
| * Cancels all the timers | ||
| */ | ||
| cancelBatchTimers() { | ||
| _.each(this.timers, timerID => clearTimeout(timerID)); |
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.
NAB, feel weird to have this single line method. Its only used in one spot and already has a comment above explaining what it does so it feels like this _.each(this.timers, timerID => clearTimeout(timerID)); could directly be used here. Not sure if you placed is separately for readability but the comment already helps with that.
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.
Yeah that makes sense I was going to trigger this from two spots at one point then changed my mind.
| }} | ||
| onChangeText={this.updateSearch} | ||
| onClearButtonClick={this.reset} | ||
| onClearButtonClick={() => this.reset()} |
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.
Why this change, is it by accident?
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.
This was to fix a random error I found. not a bug or related to these changes. one of the annoying prop-types kind of errors.
chiragsalian
left a comment
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 👍
|
FYI - Conflicts |
|
Ok, so I know we got pretty far with this one. But the original issue is now closed and we aren't experiencing the problem linked there (slow start up times for iOS apps). So we should probably leave this idea for a rainy day. As interesting as this solution is, it doesn't solve any problem we are currently experiencing. If anyone disagrees we can reopen. |


This fixes #443 I hope. I wasn't able to reproduce the problem locally, so maybe one of you are able to?
This PR adds an HOC which will allow a component to render a collection in batches. In the case of the report main view, the batches are based off of first the visible reports, then 5 seconds later, the rest of the reports. On the report actions view, the first batch is 100 comments, then 5 seconds later, the rest of the comments.
Tests