React-native: Improve rendering performance for complex UIs (especially on Android)

Created on 17 Jul 2018  路  9Comments  路  Source: facebook/react-native

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鈥檛 need to.

image1

Android Stale Discussion

Most helpful comment

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?

All 9 comments

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?

@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.

@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.

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

@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.

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?

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.

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.

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

aniss picture aniss  路  3Comments

madwed picture madwed  路  3Comments

jlongster picture jlongster  路  3Comments

despairblue picture despairblue  路  3Comments

DreySkee picture DreySkee  路  3Comments