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

feat: performance improvements #787

Closed
wants to merge 1 commit into from
Closed

Conversation

igorbej
Copy link
Collaborator

@igorbej igorbej commented Jan 19, 2024

This PR relates to the performance issues discussed in #120

Summary

  • I haven’t found anything that would affect iOS specifically, as brought up in the above issue
  • That being said, I saw multiple performance problems that make the app perform very poorly in general (shouldn’t be heavily skewed towards one platform though, but more on that later)
  • Since my time was limited and we’re working from different time zones, I decided my best bet was to try and resolve (or at least point out to you) as many issues as I could, hoping either:
    • whatever perf gains I could achieve would diminish the significance of the unidentified iOS-only problem, or
    • fortuitously, I resolve/ameliorate the problem, because it’s not actually restricted to iOS, but rather was only showing up on iOS in your tests due to some other quirks (like the specifics of your setup) we currently don’t know about
  • Following this strategy, I improved what was impacting the performance the most according to my tests and I managed to make some nice performance gains even with only some of the issues I observed resolved. More results below, but tl;dr is:
    • the RAM usage (on iOS, release build) is stable at around 200-300MB (previously I could easily get it to 700MB+, depending on the amount of time spent in the app)
    • there are significantly fewer instances of sudden frame drops throughout the app, and the ones that do persist are less severe
    • the performance of FlatLists is slightly better (but could be improved even further with more work)
    • CreateProjectScreen is much more responsive to user typing compared to other input-based screens (say, CreateSiteScreen, which I did not update). With more time, analogous improvements could be made for the remaining text inputs/screens that use them throughout the app and you can use CreateProjectScreen as a reference here.

Details

To reiterate and to be extra explicit: I haven’t been able to replicate an identical iOS lag/freeze as the one in #120, but judging by how that looks in the recording, but also the app’s behaviour, its patterns of using system resources, the load it puts on the main and the JS threads, my current theory is that it’s not a singular issue causing the lags, but rather multiple unrelated issues compounding to slowly but surely smother the app (especially the JS thread). And that results in lags and unresponsiveness like the one you saw in your tests, where the app’s business logic part (i.e. JavaScript) is so overwhelmed it doesn’t appropriately process the touch events (i.e. button presses) coming to it from the native world.

The issues

Of the issues I found: some I resolved immediately; some, specifically ones that required more time than I had available or some discussions/decisions on your end, I solved partially and/or marked in code with comments.

Here's what I've noticed:

  • Creating a new site redirects the user back to the HomeScreen but it’s not the original HomeScreen, a new one is created each time a new site is created, which quickly gobbles up system resources
  • There are at least two very non-performant FlatLists in the app:
    • the one on the HomeScreen, in the BottomSheet, which displays a list of sites
    • the one on the ProjectListScreen, which displays a list of projects
  • At any given moment the app renders a lot of elements that are not displayed in the viewport, e.g. because they belong to a different screen, etc. (HomeScreen, ProjectViewScreen or LocationDashboardScreen are good examples of this; you can investigate it with React Native's iOS performance monitor.)
  • The process of memoization is a two-step one and in many places it is only implemented halfway through, meaning the memoization doesn't have an effect. (WIP)
  • Formik forms use input fields that:
    • make the whole (heavy) Form component rerender with every keystroke, rather than only rerendering the input field
    • are controlled (i.e. receive a value prop) which is mostly fine on the web, but can be problematic for React Native due to its asynchronous nature and the fact the JS <> Native communication is happening over the bridge (see here and here for more context). And since the app is struggling already, it is indeed problematic here, resulting in input fields being extremely laggy, sometimes to the point of updating only after a multiple-second delay
  • NativeBase components are used extensively which is especially painful in the case of FlatList items, which should be as lean as possible

iOS vs Android

I would argue that Android suffers from the same performance problems as iOS as they are mostly JS-specific, and during my tests I could, for example, easily crash the Android emulator (debug build, Google Pixel 3) due to insufficient RAM after creating just a handful (~4-5) of new sites. Frame drops and some lags were also present.

That being said, the particular interaction of switching back and forth between the HomeScreen and ProjectListScreen tabs, which in your implementation uses the Stack navigator (rather than the Bottom Tabs navigator appropriate for this use case, which we've talked about in #698), is different on Android and iOS (a sort of fade-in on Android vs a slide from the edge of the screen on iOS), as the underlying native primitives also differ. And these differences might be the reason why the overall poor performance of the app affects this interaction unevenly on the two platforms. Although I have to say I'm speculating here, this isn't a conventional use case and I'd encourage you again to consider using Bottom Tabs for your bottom navigation (or Material Bottom Tabs if animations/transition effects are what you're looking for).

Results

iOS

  • Memory usage, release build
Before After
ios memory before ios memory after
  • Number of views rendered, debug build
Before After
ios views rendered before ios views rendered after
  • Other stuff
frame drops 3gb ram usage
Serious JS thread frame drops Extreme RAM usage on the latest main (debug build, but still)
  • Text input recordings
    (WIP)

Android

As noted earlier, I had no trouble getting Android to 700MB+ memory usage too, at which point it was crashing:
android

Providing it with more memory resulted in bigger consumption:
Screenshot 2024-01-25 at 13 58 50

Plus the overall experience was laggy, just like the iOS one, therefore I'm sceptical all of these issues are relevant for iOS exclusively.

Other remarks

  • The bottom navigation could benefit from some visual feedback when the tabs are pressed
  • I agree with @CourtneyLee333 that the map preview for SiteCard items isn't very beneficial for the users, and considering that FlatList items should be kept as lightweight as possible I recommend you remove it. Plus it's very glitchy visually during rendering on iOS in my experience.
  • As another avenue to explore in terms of performance you might consider looking at your bridge. (But I'm not an expert here and I'd leave that for later and take care of the more obvious issues first.)

Checklist

  • Corresponding issue has been opened
  • New tests added

Related Issues

Relates to #120

Verification steps

App should be relatively usable (interactions take at most 1 second to render).

(^Added by @shrouxm, I sincerely hope the app can clear this bar with these changes, but it is possible that more work will be required to achieve that for every scenario/interaction.)

@@ -163,12 +164,17 @@ export const EditForm = ({
);
};

export default function Form({editForm = false, onInfoPress}: Props) {
// TODO(performance): Adjust types
Copy link
Member

Choose a reason for hiding this comment

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

What sort of adjustments are needed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Nothing major, just regular props type adjustments I omitted here to save a bit of time

Copy link
Member

@shrouxm shrouxm left a comment

Choose a reason for hiding this comment

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

thanks igor! the suggested changes all make sense, i left a couple questions about the details of implementing some of them if you have time to respond. if you don't have time to keep working on the PR i'll take it over

were you able to benchmark the app and identify any performance bottlenecks any particular performance bottlenecks? and did the changes you made here make a significant difference in iOS performance or is there still more work to do to get things usable on that platform?

Comment on lines +92 to +98
{/* TODO(performance):
1. All text inputs should be adjusted into uncontrolled components
- https://react.dev/learn/sharing-state-between-components#controlled-and-uncontrolled-components
- https://github.com/facebook/react-native/issues/20119
2. The code should be rewritten/reorganized/memoized so that a change to a text input's value doesn't
trigger a re-render of the whole Form/Component (chokes the JS thread and results in terrible frame drops)
*/}
Copy link
Member

Choose a reason for hiding this comment

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

could you expand on how this would work? the form components aren't controlled by props when they're in a form, and are memoized. or are you saying that the entire architecture of Formik is causing the forms to re-render on each keystroke and we need to move away from the library?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sure, I'll try to elaborate on this further next week

Comment on lines +46 to +52
// TODO(performance): Consider adjusting ProjectPreviewCard with the design team
// to always have a fixed height so you can leverage getItemLayout (example below):
// const getItemLayout = useCallback((_: any, index: number) => {
// const length = ITEM_HEIGHT + SEPARATOR_HEIGHT;
// const offset = length * index;
// return {length, offset, index};
// }, []);
Copy link
Member

Choose a reason for hiding this comment

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

this is kind of a general UI question, but if you're specifying fixed heights in pixels in advance, how do you handle something like a user who has set a large font size preference for accessibility reasons?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Need to look into that, but off the top of my head, I think it doesn't have to be fixed-fixed but rather deterministic and repeated for all items, great question though

Copy link
Member

Choose a reason for hiding this comment

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

i see, so it'd work but you'd need to find a way to compute that dynamically. i've run into this problem with other UI frameworks before, never understood why there can't be an option to say "i promise they'll all be the same dimension" instead of "i promise they'll all be this dimension" so the framework can just measure one item's layout and use that value for the rest of the list which seems much less error-prone

Copy link
Member

@shrouxm shrouxm left a comment

Choose a reason for hiding this comment

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

thank you @igorbej! i've started working on my own PR #823 based on this one with further work, including replacing some of our usages of NativeBase components. i rebased this PR onto current main, but just saw you said you might do some more work this week so hopefully that won't cause any conflicts! (the rebase was conflict free so i think it'll be ok). you can check out my PR to make sure we aren't doing any overlappping work, besides what's pushed there i'm now just working on replacing some more NativeBase components because that seemed to be the most impactful change in terms of overall performance improvements (as opposed to fixing bugs with specific interactions). if you don't have time, i'm happy to take over this PR and merge it this week!

@shrouxm
Copy link
Member

shrouxm commented Jan 30, 2024

@igorbej one thing i was curious your opinion on, i enabled react navigation's react-freeze support in my PR. it made a huge difference with a few problem screens, but it's marked experimental, do you have any thoughts on pros/cons of the feature for our app?

@igorbej
Copy link
Collaborator Author

igorbej commented Jan 31, 2024

@shrouxm heads-up that I'll be looking into this one and your comments again today/tomorrow, but right off the bat, I just wanted to let you know that (as a general rule) you should feel free to adjust/reuse/merge anything that I contribute as you see fit. As long as I have some kind of update to know what you're working on, I'll be fine — if I have anything to commit, I can always create another PR, and I definitely don't want to block you folks 😄

@shrouxm
Copy link
Member

shrouxm commented Feb 1, 2024

@igorbej ok sounds good! some updates from today:

  • i built a new iOS release based on my branch which is stacked on your branch
  • things are looking wayyy better on my device, so i released it to teammates to validate it's also working on the devices that originally reported the worst issues
  • if it looks good there too, then i'm just going to focus on getting these PRs mergeable and validate that there aren't any regressions
  • i've made a lot of progress on my branch in the last couple days so i'd say if you do want to make more code changes, base them on my branch rather than continue to update this one
  • but i think it'll make sense to wait and see if we do actually need to put more effort into this right now, because if perf is looking ok then it'll definitely be higher prio to avoid regressions and finish building out the features for MVP than try to eke out some more frames

@shrouxm
Copy link
Member

shrouxm commented Feb 1, 2024

@igorbej ok team confirmed this is resolved on all (our) devices so we're switching focus back to feature work & no need to keep working on it! i'm just going to spend today getting these PRs merged and fixing any style regressions

thank you so much for your insight on all of this!

@igorbej
Copy link
Collaborator Author

igorbej commented Feb 5, 2024

@shrouxm Great to hear that! I didn't manage to carve out enough time to come up with a meaningful update yet, but I'm still dabbling (checked out your branch, looking into those Formik forms re-renders atm), and I'll continue to for as long as I have some availability on my hands, as I appreciate the effort and the project 😁 I'll let you know when I have something to share, and if anything comes up in the meantime, feel free to hit me up here or on Slack. Cheers 😄

@shrouxm shrouxm force-pushed the feat/performance-improvements branch from e67891a to b606b79 Compare February 6, 2024 21:07
@shrouxm
Copy link
Member

shrouxm commented Feb 7, 2024

closing as these changes were merged into main with #823

@shrouxm shrouxm closed this Feb 7, 2024
@shrouxm shrouxm deleted the feat/performance-improvements branch February 7, 2024 20:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants