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

Create REPORT_METADATA onyx key to separate report loading logic from the report data #26173

Closed
wants to merge 24 commits into from

Conversation

ospfranco
Copy link
Contributor

@ospfranco ospfranco commented Aug 29, 2023

We are trying to optimize the ReportScreen. There are some re-renders on parent components that cause a cascade of re-renders on the screen and the list of actions. This PR takes care of certain issues:

  1. Turns out the items in the list are connected to the Report object, but this object has been augmented with loading states while the actions are being fetched from the backend, which causes a lot of items to unncessarily re-render themselves. I have extracted the loading state into a separate Onyx collection, which allows to keep the logic intact but gets rid of unnecessary renders on every component that is connected to a Report.

Explanation of the graph is: The first rows are the flushing of React queue, the following bars are the rendering of the bigger components ReportScreen, ReportActionsView, etc. The last row is the rendering of individual actions in the list.

You can see the entire screen is not flushed while a lot of actions are being re-render over and over again:

CleanShot 2023-08-30 at 09 18 00

After the changes, we have regained some of the actions render cycles, but more importantly the flushing happens a bit faster (navigation mounting), so the screen actually feels smoother when navigating in/out.

CleanShot 2023-08-30 at 09 18 28
  1. A shared context ReportScreenContext was used to manipulate certain lists, and would cause parent components to re-render causing a cascade of cyclic renders. By separating the context we limit the re-renders to the part of the tree that consume the specific contexts and win back some render cycles.

here is the react devtools trace, you can see before the change there are at least 7 renders, this is also a parent component, so it triggers a cascade of re-renders on the children. Afer the change this has gone down to 3.

CleanShot 2023-08-29 at 10 16 46

CleanShot 2023-08-29 at 10 15 44

  1. The functionality of marking messages as unread was using an object to get around a useCallback on the renderItem function of the action list not updating properly. Also causing a full list re-render. The new implementation uses a simple integer to avoid triggering re-renders when nothing has changed.

Here is the trace before and after

CleanShot 2023-08-29 at 10 21 59
CleanShot 2023-08-29 at 10 23 08

You can see a reduction of 2 renders, but again this being a parent component it triggers a re-render of the entire list elements

  1. SidebarLinks was using the withReportIDContext, but it was not necessary since it only needs the reportID and that can be passed from a parent. Passing it by prop also saves some render cycles and again this is a parent component

Before:
CleanShot 2023-08-30 at 12 31 56

After:
CleanShot 2023-08-30 at 12 32 58

  1. date-fns locale was being set whenever the list of report actions was being rendered, this caused the first rendered to be delayed by at least 250 ms. I'm now eagerly trying to load it after the sidebar links have been loaded and the JS context is iddle. This results in the first report window that opens to be a lot faster.

You can see the spike created by setting this locale in the DevTools:

CleanShot 2023-08-30 at 12 49 16

Fixed Issues

$ #26087

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android / native
    • Android / Chrome
    • iOS / native
    • iOS / Safari
    • MacOS / Chrome / Safari
    • MacOS / Desktop
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that the left part of a conditional rendering a React component is a boolean and NOT a string, e.g. myBool && <MyComponent />.
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text shown in the product is localized by adding it to src/languages/* files and using the translation method
      • If any non-english text was added/modified, I verified the translation was requested/reviewed in #expensify-open-source and it was approved by an internal Expensify engineer. Link to Slack message:
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is approved by marketing by adding the Waiting for Copy label for a copy review on the original GH to get the correct copy.
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.js or at the top of the file that uses the constant) are defined as such
  • I verified that if a function's arguments changed that all usages have also been updated correctly
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately and each prop has a /** comment above it */
    • The file is named correctly
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • If we are not using the full Onyx data that we loaded, I've added the proper selector in order to ensure the component only re-renders when the data it is using changes
    • For Class Components, any internal methods passed to components event handlers are bound to this properly so there are no scoping issues (i.e. for onClick={this.submit} the method this.submit should be bound to this in the constructor)
    • Any internal methods bound to this are necessary to be bound (i.e. avoid this.submit = this.submit.bind(this); if this.submit is never passed to a component event handler like onClick)
    • All JSX used for rendering exists in the render method
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(themeColors.componentBG))
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.
  • I have checked off every checkbox in the PR author checklist, including those that don't apply to this PR.

Results

All in all, after all these optimizations here is the navigating in/out of the screen:

CleanShot.2023-08-30.at.13.33.47.mp4

@ospfranco ospfranco requested a review from a team as a code owner August 29, 2023 11:08
@melvin-bot melvin-bot bot removed the request for review from a team August 29, 2023 11:08
@melvin-bot
Copy link

melvin-bot bot commented Aug 29, 2023

@deetergp Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot melvin-bot bot requested a review from deetergp August 29, 2023 11:08
@ospfranco ospfranco changed the title create new onyx metadata collection and connect components ReportScreen, improve performance by separating loading state of report actions Aug 30, 2023
@ospfranco ospfranco changed the title ReportScreen, improve performance by separating loading state of report actions ReportScreen performance improvements Aug 30, 2023
@mountiny
Copy link
Contributor

@situchan could you run the checklist on this one please?

@deetergp feel free to review if youa re interested cc @tgolen since this is a new Pr form this which Oscar closed #25948 probbly to make it cleaner with the functional component

@situchan
Copy link
Contributor

Unit Tests failing

src/libs/actions/Report.js Show resolved Hide resolved
src/pages/home/ReportScreen.js Outdated Show resolved Hide resolved
Comment on lines 366 to 368
{/* Note: The ReportActionsSkeletonView should be allowed to mount even if the initial report actions are not loaded. If we prevent rendering the report while they are loading then
we'll unnecessarily unmount the ReportActionsView which will clear the new marker lines initial state. */}
{(!isReportReadyForDisplay || isLoadingInitialReportActions || isLoading) && <ReportActionsSkeletonView containerHeight={skeletonViewContainerHeight} />}
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the loader is not really shown now in the app

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think nothing is shown not even the header because of onyx blocking the rendering, not because this conditional changed, but I can double check

Copy link
Contributor Author

@ospfranco ospfranco Aug 30, 2023

Choose a reason for hiding this comment

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

Yep, the conditional is correct on the first render of the screen, it just seems that now the actions load so fast that the skeleton is no longer rendered. But we are still working on finding more performance improvements because the screen can be mounted even faster, then the skeleton will probably start working again

src/pages/home/report/ReportActionsList.js Outdated Show resolved Hide resolved
@situchan
Copy link
Contributor

situchan commented Sep 4, 2023

@mountiny we're holding this until merge freeze is over, right?

@mountiny
Copy link
Contributor

mountiny commented Sep 4, 2023

@mountiny we're holding this until merge freeze is over, right?

Yes while this would be great to have, I think it has higher risk of regression slipping through the testing so I agree

@fedirjh
Copy link
Contributor

fedirjh commented Sep 14, 2023

Overall summary

Fixed


Not Fixed


CleanShot.2023-09-14.at.17.53.22.mp4
CleanShot.2023-09-14.at.18.02.36.mp4

@ospfranco
Copy link
Contributor Author

I'm very sure the last two reported bugs have nothing to do with my changes, but it's up to you @mountiny, I can take a look tomorrow if you feel like this should be fixed as part of this PR.

@fedirjh
Copy link
Contributor

fedirjh commented Sep 14, 2023

The layout glitch is reproducible on main , I have checked that 👍🏼

Copy link
Contributor

@fedirjh fedirjh left a comment

Choose a reason for hiding this comment

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

Let's remove the redundant code and this should be ready to get merged.

@@ -245,6 +253,18 @@ function ReportScreen({
[route],
);

const calculateSkeletonViewHeight = (event) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

This code become redundant after this commit ffaf7a8 , As for the other component that uses skeletonViewContainerHeight , It's ReportActionsView, does not have a prop called parentViewHeight, I bet it was mistakenly added in this PR #12169
Let's just remove it .

parentViewHeight={skeletonViewContainerHeight}

@mountiny
Copy link
Contributor

@ospfranco @fedirjh tahnk you very much for a quick work, lets get this ready to get merged

@ospfranco
Copy link
Contributor Author

Redudant code removed :)

@mountiny
Copy link
Contributor

@fedirjh The build should be running for final tests, we are close 🤌

@fedirjh
Copy link
Contributor

fedirjh commented Sep 15, 2023

@mountiny seems like the build was not triggered ? Should I run the reviewer checklist locally?

@OSBotify
Copy link
Contributor

# Conflicts:
#	src/pages/home/ReportScreen.js
@ospfranco
Copy link
Contributor Author

Merged main to resolve conflicts

@fedirjh
Copy link
Contributor

fedirjh commented Sep 18, 2023

@ospfranco I have reproduced this bug #26173 (comment) , on Mac/Chrome and Mobile web/chrome, with 2 different accounts :

CleanShot.2023-09-18.at.09.26.09.mp4

On native , the report is not displayed immediately : a momentary empty view appearing between the skeleton and the report. This issue is also present on the web, but it's less noticeable due to a quick transition . This behavior seems to stem from the report data being split into two collections, and any updates to each collection trigger a re-render. It's unclear whether we can completely eliminate this issue, but I'd like to hear your thoughts on possible solutions.

CleanShot.2023-09-18.at.09.28.36.mp4

@fedirjh
Copy link
Contributor

fedirjh commented Sep 18, 2023

I have reproduced this bug #26173 (comment) , on Mac/Chrome and Mobile web/chrome, with 2 different accounts :

It's unclear whether this bug is related to the current PR. I was unable to reproduce it locally. It seems more like an issue with the ad-hoc build.

@mountiny
Copy link
Contributor

It's unclear whether this bug is related to the current PR. I was unable to reproduce it locally. It seems more like an issue with the ad-hoc build.

I also reproduced this on the adhoc build but I am not sure if this is problem with the pr or with the build

This issue is also present on the web, but it's less noticeable due to a quick transition . This behavior seems to stem from the report data being split into two collections, and any updates to each collection trigger a re-render. It's unclear whether we can completely eliminate this issue, but I'd like to hear your thoughts on possible solutions.

This is not looking that great I agree, the updates should be right after each other right, since its successData which is applied and removes the skeleton and then the report data should be shown immediately

@ospfranco
Copy link
Contributor Author

It's unclear whether this bug is related to the current PR. I was unable to reproduce it locally. It seems more like an issue with the ad-hoc build.

I also reproduced this on the adhoc build but I am not sure if this is problem with the pr or with the build

I'm not able to reproduce the issue, however nothing of my changes should affect the behavior after log in

This issue is also present on the web, but it's less noticeable due to a quick transition . This behavior seems to stem from the report data being split into two collections, and any updates to each collection trigger a re-render. It's unclear whether we can completely eliminate this issue, but I'd like to hear your thoughts on possible solutions.

This is not looking that great I agree, the updates should be right after each other right, since its successData which is applied and removes the skeleton and then the report data should be shown immediately

This is rather a problem with the component tree being too slow to render due to all the stuff we already discussed and should be far less noticeable or even not there at all when all the performance improvements have been merged. However, you seem to be running in simulator, performance on a release build should make this less noticeable.

@fedirjh
Copy link
Contributor

fedirjh commented Sep 18, 2023

However, you seem to be running in simulator, performance on a release build should make this less noticeable.

I already tested the android build and got the same result

andro.mp4

@ospfranco
Copy link
Contributor Author

Let me check if there is anything I can do about it with the current changes

@ospfranco
Copy link
Contributor Author

Added one more conditional to keep the skeleton view mounted while the actions are being layouted. @fedirjh give it a try.

@fedirjh
Copy link
Contributor

fedirjh commented Sep 18, 2023

@ospfranco We have a lint error.

Switching between loaded reports briefly displays a skeleton.

CleanShot.2023-09-18.at.12.01.38.mp4

@ospfranco
Copy link
Contributor Author

Ah, yeah, then it doesn't work. The real issue is the mounting speed of the component, everything else is a workaround. If it is really a blocker, then I suggest it is better to close this PR and wait until the onyx one is merged (should be soon), and work on #26772 as it will massively improve performance on the mounting of the list component.

@fedirjh
Copy link
Contributor

fedirjh commented Sep 18, 2023

cc @mountiny, what should be next steps in this case ?

@mountiny
Copy link
Contributor

Alright, I think this would be a step back if we would merge this and on web we would get the skeleton flicker for cached pages too (now it loads instantly).

If there is no way around that and we need to wait for the initialValues improvement, then I think lets close this PR and focus on that, I can assign you to that PR as well @fedirjh and double the price to cover for this too

@ospfranco
Copy link
Contributor Author

Alright, closing this

@ospfranco ospfranco closed this Sep 18, 2023
@ospfranco ospfranco deleted the osp/report-metadata branch October 10, 2023 11:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants