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

Improve rendering performance for complex UIs (especially on Android) #20254

Closed
taylesworth opened this issue Jul 17, 2018 · 9 comments
Closed
Labels
Platform: Android Android applications. Stale There has been a lack of activity on this issue and it may be closed soon. Type: Discussion Long running discussion.

Comments

@taylesworth
Copy link

For Discussion

We have a React Native app that renders custom forms fetched from a server. These UIs can be arbitrarily complex, with multiple columns containing potentially many (hundreds) of components, including date-time inputs, images, dropdowns, etc.

For a native version of this app, we would use UICollectionView on iOS and RecyclerView on Android. Unfortunately, the current VirtualizedList implementations in React Native are not conducive to the types of complex layouts we need to render. And the performance when not using a VirtualizedList can be quite bad, especially on Android which we have measured as taking at least 2x as long to render as iOS.

Possible solutions that we believe would help:

  1. A more powerful virtualized container that knows which components should be rendered on the screen and manages the process of fetching and rendering only the necessary views as the user scrolls. Something akin to an iOS UICollectionView which treats layout and rendering as separate tasks and keeps a pool of reusable views. Ideally this new virtualized container would be implemented at the native layer and maybe hook into the existing UICollectionView and RecyclerView containers provided by iOS and Android.

  2. We believe that even with the current implementation, performance on Android could be improved with a more efficient interface between the Java and native layers. There is quite a bit of overhead when using JNI and there appear to be many more Java-native calls being made than need to be. The following performance profile shows that iteration calls [1] and array size retrievals [2] were both long-running calls. The latter actually fetches the full array to calculate the size, even though the array itself isn't used. In some cases having it fetch the full array makes sense. However the initial size call shouldn’t need to.

image1

@hramos hramos added the Type: Discussion Long running discussion. label Jul 17, 2018
@react-native-bot react-native-bot added the Platform: Android Android applications. label Jul 17, 2018
@sahrens
Copy link
Contributor

sahrens commented Jul 17, 2018

Lots of great points - thanks for the thoughtful analysis!

We have some longer term projects looking to address a lot of these issues, but you might be able to improve things in the short term as well.

Can you describe more specifically the concrete performance issues you're seeing? Is it that initial render is taking too long? Or are you seeing dropped frames while scrolling? Both? Something else?

@taylesworth
Copy link
Author

@sahrens The biggest problem we are seeing is on initial render. We believe that is due to the native layer creating views for every component as part of initial layout. This also causes memory pressure on particularly large forms that we believe also could be avoided with a virtualized container that acts more like UICollectionView.

@davidlongest
Copy link

davidlongest commented Jul 18, 2018

@sahrens The flame graph in in the description above is one I captured from an initial render on a Nexus 5X of an especially complex UI. It helped exaggerate the performance issues we were seeing. There are also render time issues with a virtualized list where we'll get chunks of empty scrolling area while waiting for the mqt_native_modules thread to complete its view creation and layout.

That being said, once the views actually get added to the screen on Android it is very performant. We typically see it maintaining 60fps with only a short stutter after the mqt_native_modules thread is finished processing the render.

Edit: Also render times on a Moto G4 Plus is especially bad due to the low clock rate on a single thread.

@sahrens
Copy link
Contributor

sahrens commented Jul 19, 2018

These are known patterns that can be tweaked for different work loads and has been found to work quite well in many scenarios, so I think you'll be able to get it working well for you too. You'll most likely see the most success by optimizing your product code, though - one of the best bang-for-your-buck optimizations is going to be proper use of PureComponent or shouldComponentUpdate to make sure you're not doing wasteful re-renders, but also general profiling and optimization of your JS code and react component tree.

Once you've done that and you're still seeing issues, the next thing to do is look at the size of your rows and limit your first render to just the content that fits on the screen (or less, if you want) with the initialNumToRender prop of FlatList/VirtualizedList. If you have a constant or pretty consistent heights for your rows, then just set initialNumToRender={screenHeight / avgRowHeight}. If they are wildly different and driven by the data, then ideally you could do some ad-hoc processing of the input data and try to estimate how many items you want to render initially. Or you can just be conservative and always do 1 or 2 or something (the default is 10).

As for blank content and memory pressure, there are a couple levers you can pull in VirtualizedList , but unfortunately they are often at odds. To reduce memory consumption, you can reduce windowSize so less content is held in memory at any given time (default is 21 screens worth of content, 10 above and 10 below), but this may exacerbate the blank content problem (also note that when content is virtualized, the instance and it's internal state are lost, so if you have state you need to preserve as rows scroll in and out of the virtualization window, you need to hold it outside the component, like in Redux or something). You might also be able to tune things to fit your data pattern with maxToRenderPerBatch - if your rows are really big and slow, then reducing this will make the system able to adapt more quickly to changes in the target window rather than getting stuck rendering content that's no longer needed because you scrolled past it.

I'm glad you're seeing good framerates, though - that's the fundamental advantage of the VirtualizedList architecture which keeps everything as async as possible with almost no chance of blocking the main UI thread. The disadvantages though are that it's possible to scroll faster than the fill rate and (1) see blank content (note the alternative would be to drop frames until the content was rendered), and to mitigate the blank content issue, the virtualization window needs to be a little larger than e.g. UICollectionView which (2) uses a bit more memory.

Let me know if any of this could be clarified in the docs:

https://facebook.github.io/react-native/docs/flatlist
https://facebook.github.io/react-native/docs/virtualizedlist

@taylesworth
Copy link
Author

taylesworth commented Jul 19, 2018

@sahrens Thanks for the feedback. I can assure you that we have done everything you recommend before posting this. We have spent significant time optimizing our React code such that for the majority of the cases, the performance is fine (although better on iOS than Android).

We use PureComponent and shouldComponentUpdate everywhere. And we make good use of VirtualizedList for rendering grids where the data to be rendered is consistently shaped, and for that use case it is fantastic.

However, as I said in my initial post, VirtualizedList is not a particularly good substitute for UICollectionView for rendering arbitrary, complex layouts where data is not consistently shaped. We have tried very hard to make VirtualizedList work for some of the more complex forms our customers can create but, as you said, there are tradeoffs so what works effectively for one UI won't work for another.

We are very hopeful that some of the work being done as part of Fabric will help in this regard but of course we have very limited visibility into what is being done other than watching the PRs that merge into this repo with interest.

@sahrens
Copy link
Contributor

sahrens commented Jul 19, 2018

I'm not sure how UICollectionView or a similar component/API would help you with initial render then - can you expand on that? Is it possible to break down those complex forms into more granular rows?

@taylesworth
Copy link
Author

I believe the advantage that UICollectionView provides is that it separates layout from the actual creation of views. I suspect that a lot of the time spent doing the initial render, on iOS anyway, is creating the views needed for every component in the form.

We have a simplified fully-native implementation on iOS that parses our form JSON into a model that maps components into sections and columns. We then do a layout pass in a custom UICollectionViewLayout that uses that model to create UICollectionViewLayoutAttributes for every component. At that point, we know where every view will be positioned in the UICollectionView without having created the actual views.

The UICollectionView then queries our custom UICollectionViewLayout to determine which views to render in the current scroll viewport. This all happens very fast, is very memory efficient, and doesn't require heuristics about how many views to render initially.

That said, I am assuming that the time-consuming part of the React Native initial render is creating all of the views, rather than time spent in Yoga calculating layout for all of them. One of the reasons we love React Native is being able to use flex for our layout, which is admittedly more complicated than our simple native layout logic.

But I suspect that using Yoga to build UICollectionViewLayoutAttributes for each component would be much faster than the creation of shadow node views for layout. It would certainly be more memory efficient.

I'm happy to collect more detailed profiling data for initial rendering of our forms on iOS if that would help, similar to what my colleague David did on Android.

@stale
Copy link

stale bot commented Feb 2, 2019

Hey there, it looks like there has been no activity on this issue recently. Has the issue been fixed, or does it still require the community's attention? This issue may be closed if no further activity occurs. You may also label this issue as "For Discussion" or "Good first issue" and I will leave it open. Thank you for your contributions.

@stale stale bot added the Stale There has been a lack of activity on this issue and it may be closed soon. label Feb 2, 2019
@stale
Copy link

stale bot commented Feb 9, 2019

Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please feel free to create a new issue with up-to-date information.

@stale stale bot closed this as completed Feb 9, 2019
@facebook facebook locked as resolved and limited conversation to collaborators Feb 10, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Platform: Android Android applications. Stale There has been a lack of activity on this issue and it may be closed soon. Type: Discussion Long running discussion.
Projects
None yet
Development

No branches or pull requests

5 participants