Before I jump on the "React Native is slow" bandwagon


#1

Prologue

“a poor craftsman blames his tools” - Idiom

While I think a poor craftsman blames his tools, it’s not lost on me that a poor craftsman chooses bad tools, I don’t think React Native was a bad choice given the historical context of a small team having to support multiple platforms and given the options available at the time.

The tech stack for an alternate client or Status 2 is certainly open for discussion, as in the heated discussion (React-Native vs native mobile app) , but not the point of this post.

I want to question the ‘React Native is Slow’ meme that is spreading within the organisation. This meme is harmful, as I believe most developers are using this as a heuristic rather than using critical thinking to solve problems. Based on my last call with design, the meme has now also spread to designers, which is now prematurely limiting their design decisions and preventing Status from being even more visually stunning.

Historical Context

tldr: skip to Status’ UI Performance

It’s no secret, but yes, at one stage of my life, I was blessed with the curse of being a Flash Developer. I wrote the first ever Inverse Kinematic Bone Tool system for Adobe Flash MX 2004 before it became a defacto tool, I moved onto Flash Develop where I wrote and contributed to 3D Engines for Flash where we could push for 60-120 FPS. I also used to write in FLASM, AVM and AGAL to push Flash to its very limits. Writing exploits was also kind of fun at the time. Eventually lead me to Haxe which was adopting ECMAScript language features that was way ahead of its time, compared to javascript.

This was all before 2009, back when Internet Explorer 6 was a thing.

Why do I mention this?

Because in 2009 there was a meme that built up around Flash saying it was bad, slow and non-performant. The final nail in the coffin was Steve Jobs infamous post. Now I’m not objecting that this is a truism, but Flash let you do some amazing stuff that couldn’t be done in-browser otherwise, and if you took the time to wield it, it was an amazingly performant tool.

While the tool is partly to blame, the bigger problem was the usage of Flash, the problem with tools that lower the barrier to entry is you get a bunch of low-tier developers flooding into the space.

Everyone was looking to HTML5 as the Flash Killer, which of course is what the developer base migrated to, leading to articles like Electron is flash for the desktop a few years later. Hmmmmm!

The more modern the application development tool, the higher the abstractions and the worse it seems to be, there seems to be a trend of software bloat matching the resources of the time, I guess the argument is we trade-off developer productivity. I personally would like to go the other way which is why the Nim+<GUI> approach is interesting. The language gives you pythonish developer productivity and it’s closer to the metal. Okay yeah Nim’s community & library support is lacking which might make it a bad choice. Anyway I digress.

The point is, this persisting meme is blaming the tools, but really it’s a reflection of ourselves. We have alot of talented people in Status and I think we can do better.

Status’ UI Performance

It’s no secret that Status is slow, it’s getting better, but this ‘React Native is Slow’ meme serves as a crutch that acts more like a hindrance, mentally preventing critical thought into our code’s behaviour.

Our UI is not so complex, so it should be a real concern to everyone involved that it’s not performing well.

I looked into this awhile back and it became very obvious that we haven’t done everything to make Status performant, I would like to revisit it.

5 min search, found some helpful links on how to look at performance within React Native.

and Profiling Android UI Performance for React Native , which isn’t loading a preview for some reason.

Overdraws

Overdraws basically see how many times you are drawing a pixel every frame, if you write the same pixel more than twice a frame it’s considered bad (I’d say more than once is bad imo)

If you’re on Android this is easy to see:

  • Enable Developer Options (go into About and tap build number 6+ time or something)
  • Settings > System > Developer Options > Debug GPU Draw = Show Overdraw Areas
  • Open Status
  • Blame React Native for being slow. :slight_smile:

For your reference here is the colour chart:

image

Here is two screens of Pinterests Topic Chooser, a screen they claim is 100% in React Native on all platforms. One is Static and One is me Scrolling.

and here is Skype, which is also in React Native (although to be fair it might be in Microsoft’s variant now)

I found it interesting that the Skype login Webview overdraws are high, but the login screen and list itself isn’t, even when scrolling.

Now here is Status:

The Splash Screen is Native, the spinner and the rest is React Native afaik, it seems to me Status renders it’s entire screen more than 4 times, every frame. This isn’t React Native, this is our code. If we reduced this to under 2x overdraws we would likely see a 2x performance increase app-wide. If down to 1 would we get a 4x performance boost?

I’m not sure if this is a symptom of re-frame or some other library we use, I speculate there’s something wrong in one of our parent dom nodes (my guess is a use of transparency (???)). We need to identify the root cause of this and fix it.

Using console.log statements

So Facebook recommends to include the babel plugin transform-remove-console to strip the codebase of console.log statements as it seems to hinder performance, I don’t think we do this but I guess Closure or some other transpiler does this already for release builds?

Flatlist

Looks like we already use this which is great!

Static Animations & useNativeDriver: true ?

The Animated API currently calculates each keyframe on-demand on the JavaScript thread unless you set useNativeDriver: true , while LayoutAnimation leverages Core Animation and is unaffected by JS thread and main thread frame drops

I couldn’t find any references of using this in status-react, is there anywhere that makes sense for us to utilize this?

shouldRasterizeIOS or renderToHardwareTextureAndroid

Moving a view on the screen (scrolling, translating, rotating) drops UI thread FPS. This is especially true when you have text with a transparent background positioned on top of an image, or any other situation where alpha compositing would be required to re-draw the view on each frame. You will find that enabling shouldRasterizeIOS or renderToHardwareTextureAndroid can help with this significantly.

I couldn’t find references to this either in our codebase, I’m not sure if the reason we don’t have the screens sliding is because it was unperformant. But maybe this could help if we decide to reintroduce it, it might also help with any transparency used in the application

Battery Consumption

We seem to be running the node in the background on Android, but not on iOS, afaik we are not taking full advantage of this, so can we improve battery life (and prevent the battery warnings) by not running the node in Background on Android?

Push Notifications to Chat Screen

The flow between tapping a push notification and landing in chat is pretty bad, it takes about 10-15 seconds to land in chat and have it’s history populate, feels very similar to the now resolved slow sign-in.

Fin’

Anyway, I’m not that familiar with our codebase, or how to squeeze the performance out of React Native, but it seems like the overdraw’s are definitely an issue, and there’s probably more we could be doing.

I hope we can make a more concerted effort to making Status as snappy and responsive as any other application. And once we have, and the UI performance still sucks, then our trash talk about React Native is justified and I’ll happily jump on the bandwagon.

If this is something we can’t solve, would love to know the reasoning.


#2

Thanks for this research! I’m one of the people who is skeptical about React Native performance.

One important thing I want to notice is that: react native isn’t slow per se, it is just way less forgiving for performance issues. When using pure native components you usually don’t even need to use any of the advanced performance optimization techniques to get a decently responsive app. With RN, if you want snappines, it is not there by default. That doesn’t mean that you can’t do a responsive app with RN, but it is just much harder.

Any RN app can use native components while using RN for orchestraton (like AirBNB did), so it might be that, hard to tell without source code. Still worth looking into.

Yes, I agree that overdraws are a problem, but I don’t think that dependency is linear.

We sure use native driver, you can grep our codebase for useNativeDriver. Also, what isn’t said is that native driver didn’t support all animation types last time I checked (it had problems with opacity, which plainly disabled nativedriver).

that probably makes sense to research, noted

We don’t have any background services starting 0.9.33

Agree, but that is exaggerated by a single-threaded nature of a JS engine that RN uses. If only we had threads (or queues like iOS) by default, we wouldn’t need to jump through the hoops to achieve better performance there.

When I was profiling the app ALL the perf issues (slow app) I’ve seen were
caused by a clogged JS thread. But it doesn’t mean that we shouldn’t do these optimizations.


#3

Wish I had more useful feedback to add to this thread here, but just wanted to say that I appreciate the pragmatic and research-based thread!

Good stuff and very interesting too! :ok_hand:


#4

That’s exactly why I drew the parallel to Flash. Flash was not forgiving, but it did let you run everywhere.

I know, most of the showcase articles say and demonstrate exactly this, including Pinterest, which was another example I highlighted and chose specifically the topic screen because they say it’s RN. Who knows if it is. But I don’t think you are saying that overdraws are within RN’s domain beyond our control.

My mistake I did do this, just on an old branch. Nice!

Ah dang, I still get battery warnings on 0.9.33 (2019012902), do we know why?

To expand on this @igor had mentioned in #312-janitors that we need to offload more items from the JS thread, @adam is working on migrating all the protocol stuff into Go which sounds like the bulk of it.

Would be great not to hammer the GPU all the same


#5

I think it is firebase, but needs to be checked.


#6

One other thing, is shouldComponentUpdate important to our components?


#7

Although I realise the bulk of the work is on the JS thread, also found this article with some more knobs to tweak FlatList.


#8

maybe, we need to investigate


#9

:cry: <-- tear of nostalgia.
Pretty sure I used your IK stuff then, @jarradhope. Working in Flash (and Flex) in the 00s remains the best period of my dev career to date. You could do anything you set your mind to, but like with JS people started using it for literally everything and ruined things.

Good research, looking forward to seeing if this can yield some perf upgrades.

As an interesting side note, having started new development of their app in RN, Balance.io gave up on it after a few months and figured that maintaining three separate native codebases hooking into the same cross platform components behind the scenes is way cheaper than continuing with RN.


#10

Having the same codebase in general (not RN specific) bring coordination costs up and reduces autonomy.

It is a tradeoff, and it is not as bright and shiny as usually portrayed. But yeah, each team pick it’s battles.


#11

In my opinion, React Native is the least of our problems. The architecture and how we wanted all components to work together is a problem.

We should really have three layers:

  1. Light UI layer in status-react,
  2. A layer managing messages and exposing a proper full-duplex, efficient interface to the UI,
  3. Wrapper on go-ethereum in order to connect to the Ethereum network.

(1) should run with a mock or light version of underlying layer so that the development and conducting UI/UX experiments is fast. There is really no need to run the whole node connected to other peers to implement some UI components.

(2) should be separate and heavily focused on testability and debugging without the need to run UI which really slows the process down. Ideally, it should also be pluggable in terms of the transport so that the logic can be verified without a need of Whisper network to be running. It also should connect to a node through various interfaces like a socket file or HTTP.

(3) should also be a separate but we are there with status-go pretty much. It should be possible to run in docker, on a server and also be part of the final app build.


#12

Without knowing enough about the status-react codebase but encountering similar problems in the past using react for web my first guess would be is there are components that are subscribed to portions of the state that they don’t use and when that state updates it causes those components to re-render. React best practice is to have components receive the data they need and nothing more, which actually is a good practice in general.

My second guess would be not batching some updates, ie: adding 100 new messages to the state as 100 actions causing 100 renders rather than batching them into 1 action which is 1 render.


#13

Great post and I agree with many points, especially regarding the historical context of the initial decision.

I strongly disagree with the “meme” part, though:

React-Native IS slow.

Let me explain this. Whenever I reason about any language or framework, I do this by analyzing the mental model of how it works. Coming from the C background, my mental model goes through all layers starting from actually parsing and lexing the code and down to the hardware level, where you can reason about CPU cache misses or memory alignment of data structures. (I hope all developers do the same).

When writing native apps, you normally use API of native libraries, which are highly-optimized and close to the hardware, and we normally don’t have options to change anything on that layer anyway. The native API performance can be seen as a baseline for comparing with any other language or framework on top.

Now, enters React-Native – JS framework that consists from JS and Native parts, glued together via Bridge. JS by itself is one of the worst designed languages ever, which is no surprise considering the fact it was created in two weeks under huge pressure long before the web ecosystem, let alone mobile UI, even emerged.

Its raw CPU performance is worse by order of magnitude than C/C++ native code. Adding 10x slower language on top of native API and saying there is no penalty on performance is, obviously, absurd.

Next, building our mental model of how React Native works – does anyone think that JS is being statically transpiled into its native iOS/Android counterparts by analyzing its AST code? Nope, the React-Native app is running an insanely complex bridging system, which involves, among other stuff, constant serialization/deserialization of JSONs, passing those between three or more threads, allocating tons of data and using a lot of “helper” conversions (like image coding/decoding via Base64) and reflection to convert to/from native and JS type system. Even if you “just” want to draw a box on a screen. All this happens in RN app up to hundreds of times per second and holds true for the UI aspect of apps. In fact, there is a stalled effort of eliminating this embarrassingly slow bridge by replacing current Yoga engine to the almost native JS-Native API bridging, called React-Native Fabric, but it’s very far from reality so far.

If you keep this mental model of inner workings of RN in your head, you start understanding why such simple things as drawing screens or navigating between them are listed as problems in every “React-Native performance issue” article. It’s just mindblowing amount of complexity placed onto CPU and Memory just to allow people to use JS.

I think anyone who has this mental model, should be scared of RN as a practical solution immediately, especially if there are better alternatives are available.

Now, I totally understand the argument “RN is slow, but it’s still fast enough for our needs” – it’s a perfectly valid argument. The human eye doesn’t detect lags under around 100 microseconds, so if RN makes an operation that normally takes a hundred nanoseconds slower by two orders of magnitude – that’s still can be fine.

And yet, it doesn’t invalidate the argument that RN is slow and numerous articles about performance issues of RN is evidence. You can spend many person-years to try to optimize the performance, to trade memory for CPU, to apply hacks that make slowness less perceivable, etc, etc, and yet, this is not changing engineering ugliness and slowness of the technology.

So yes, React-Native IS slow.
By design.


#14

Thanks for suggestions, @barry!

The first one is exactly what i thought of at first glance, specifically re-frame's subscriptions to the state which we are using in the app. But then, after playing a bit with a code i realized that this is most likely not a case, as we do not receive updates to application’s state with such a frequency which could cause 4+ re-renderings per frame.

In fact we can assume that app state is constant during the specific frame (there is no 100% guaranty, but still) and thus re-drawings are caused by view layer itself, not by some specific app logic.


Then i tried to figure what exactly causes re-drawings, and that’s an example of what i’ve found on :my-profile screen.

At the beginning that screen looked like this

Then i played with styles of a bunch of components and voila, our hero was background-color property of Views’ styles.

I could probably make the screen almost white by removing even more unnecessary background-color props, but i suppose it’s enough atm to demonstrate the point.


And so conclusions:

  1. This specific metric is not really affected by app’s architecture.
  2. We can reduce re-drawings on almost any screen by removing background-color prop
  3. I will be quite surprised if this action (2.) will have a huge impact on the app’s performance, but who knows!

#15

just came out, https://softwareengineeringdaily.com/2019/02/22/react-native-rearchitecture-with-g2i-team/ if anyone interested


#16

I think it would be interesting to do some performance testing with these changes. If it does have a big impact on performance, I’d have to give it to Ivan that this is worrying in terms of how easy RN makes it to have poor performance if something as simple and apparently innocuous as setting the background-color is enough to cause that many redraws.


#17

By default, a layout does not have a background, which means it does not render anything directly by itself. When layouts do have backgrounds, however, they may contribute to overdraw.

Overdraw occurs for the same colours and different colours too.

I believe we use the same background colours a lot in status-react, we could optimize this for sure


#18

Apparently overdraw and general rendering issues are more of an Android problem. From Andrey link it seems like overdraw does have an impact on Android so we should try to get rid of it as much as possible for easy perf improvement.

If you listen to the podcast linked by Andrea, it seems like Airbnb dropping react-native was mostly due to Android perfs. Airbnb had native iOS and Android teams before trying react, and the iOS team was satisfied with it. Discord on the other hand went react-native from the beginning on iOS and sticked to it with no perf issue.

The pending work on getting rid of the bridge looks very promising.

I think another aspect affecting perfs is related to database access with already lots of improvements made by Roman. On a new accounts the app is pretty smooth for me, so the rendering is only part of the issue.


#19

then let’s test it https://github.com/status-im/status-react/pull/7562#issue-255616987 :slight_smile:


#20

the logging macros clean up at compile time based on log level. The log level in prod is info for the Clojure part.
According to your first link Having **any** active console.log statements in your production build will significantly impacts the performance of the build. So we might want to use that babel plugin to be sure we remove everything in prod if any console.log statement is a potential perf issue?