(Don’t) Blame It On React Native
Abstract
It’s always fun to blame react native for everything that’s wrong in our lives. However, what we keep finding, again and again, is that in most cases, writing better performant react code fixes our app’s performance and laggy UI issues.
Part 1 — Blaming React Native
My story begins three years ago, when react native was in version 0.12, it didn’t even support android! We just made a major decision at my company (wix.com) to gamble “all in” on react native and write our main app solely in react native using as little native code as possible. You see, Wix is a web company. We have hundreds of front end developers working on tens of products. For us, re-writing these products both for ios and android was too big of a pain. We saw the potential in react native and went for it.
Jump ahead two years, we now have an amazing app for both android and ios. We’re writing and maintaining dozens of open source libraries for react native with thousands of stars, and we’re about 60 developers constantly contributing code to the Wix app.
Even though react native has improved a lot since version 0.12, things are not so peachy as they’re often portrayed. If you ever wrote a real, complex react native app on production with a lot of traffic then you know what I’m talking about.
It comes down to these two images:
I’m not going to lie, while react native gives you an easy way (if you’re a front end developer) to write native apps, writing react native is no walk in the park.
It’s hard because React Native…
- Adds an abstraction layer over native APIs
- Performance sucks
- Is single threaded
- Makes it difficult to accomplish things that otherwise are very easy in regular native apps (Efficient long lists for example)
- Build is more complex
Here’s a quote every RN developer said or thought to himself at least once
“Our only option for a truly performant app is to write a pure native app”
- Any react native developer
While indeed it’s an option, and it might be a valid one, depends on your use case. I claim it’s not the only one!
Part 2 — Aha moment
About a year ago we did a lot of performance improvements in our app. And time after time, bug after bug, issue after issue, we found that with better performant javascript and react code we could solve most of the issues. This was my “aha moment”. It changed things for me.
Let’s go over a few code snippets and case studies to demonstrate this.
Problem #1- Send button enable / disable
Part of our app includes a chat module, so site owners can talk with visitors on their site. The feature is super simple. There’s an input field and a “send” button. The button should be disabled when the input is empty and enabled when the input is not. What happened was, it took about a second (sometimes even more) for the button to become enabled after the user started typing.
We discovered that we rendered the entire list of chat rooms, even when we were inside a single chat room screen. This happened because we created a new chatroom object for each chatroom after every change in any of the other chat rooms. It might sound like a horrible mistake on our part, but when writing in a functional manner — it looks right and harmless. The solution we choose to overcome this issue was to save the unchanged chatrooms using memoization.
Problem #2- Messages arrive in delay
The second issue we tackeled was the slow rendering of the chat messages as they came in realtime. Here are two videos (before and after) showing the problem.
As you can see, there’s a substantial difference between the two versions. The problem was componentWillReceiveProps was called way too many times. And for each time it was called, it triggered a new render cycle, due to a call to setState.
Problem #3- Typing Animation
This one I really like. Our very talented and creative designers challenged us with a super slick and fancy typing animation. The kind where the bubble heads slide over each other, pushing up the previous messages, sliding up and down and transforming magicly into message bubbles. You know... any front end dev’s nightmare. :) At first, we went with the naive approach and calculated our animations on javascript using Animated. Well, that didn’t work. Because of the way Animated works, you can’t set properties such as “height” which we needed to animate while using the “useNativeDriver” flag. This meant that all of the Animated calculations were done on the javascript side which made the animation look very laggy. Read more about why you can’t change height while using the native driver here.
Then we said, wait a minute! React native gives you the promise to do anything you want in native. So we did. We tried native, but it sucked as well. While the animation itself worked very smoothly on the native side, when we tried to embed it into our react native controlled view we got into trouble. Why? Part of our animation included changing the container of the animated section’s height which in turn changed the layout of its sibling which were written in RN. And so, we were back to square one. We could’ve done the animation in the native side but then we needed to tell RN that we did so (changed the height). In turn, RN needed to do his animations based on that which he only could do on the javascript thread due to the same problem as before.
Just before giving up, we discovered that we could do it using react native’s LayoutAnimations.
Problem #4- Module load time
The last one I want to share with you is a story about a simple “require vs import”. Our app consists of many independent modules. You can think about it as if each module has a different bundle. What we found was that just delaying the import statements by requiring it in only when needed, saved us a lot of time. I’m talking about seconds across the app.
Again, all we did was remove the “import” statements from the top of the file (which you might think is harmless) and instead require what we needed, only when we needed it.
Part 3— That’s just how javascript and react works
So what am I really saying here? Basically what I’m trying to say is that react native is not to blame in any of these cases. That is just how javascript and react works. It’s just a lot easier to see the consequences in react native due to the react native bridge and the low computing powers of mobile devices.
To prove my point I was set out to create a simple react web app that will suffer from poor performance and that it would be fixable by changing very few lines of code (actually just one line of code is needed). I call this project the “Good Card / Bad Card”. It’s a very simple react app. It shows a list of cards, an total of 100, nothing crazy. Each card has an input field. In the bad card you can see the lag in the UI when you type in the input field. The keyboard just doesn’t react as you’d expect. The good card works flawlessly. I’ve put the project on github and I encourage you to check it out. The code is very simple and it’s only a single line of code that has a tremendous effect. For those of you who don’t have time, you can just check out the two versions here:
Part 4— It’s up to us
Now that we know all of this. What can we do about it? Well it’s up to us. We must be more careful and responsible react developers.
We can do that by following these guidelines:
- Know your crucial app flows and keep monitoring them
- It doesn’t matter how you monitor your app as long as you’re making sure that you got your crucial app flows covered and that you will get notified when performance degradation happens - Avoid writing anonymous functions as props
- These could potentially have a huge impact - Know your components
Don’t think that just writing PureComponents will solve everything - Know your components lifecycle methods
Each component is different and might need to use different lifecycle method
Last but not least know your reducers. For those of you that are familiar with redux. I want to show you a cool analogy between databases and reducers
Let’s start with the database. Let’s say that we have a table where we got many writes to one field and many reads to another field. Any write locks the table and delays the read. The solution is to separate the fields into two tables.
In redux, let’s say we have a lot of actions that changes a field (it can also be an inner field of some object) but not many reads for this field. Any state change triggers the rendering cycle. The solution here is the same, separate to two reducers.
Part 4 — Epilogue
By now you might be thinking well, that is nice and all but where and how should I begin profiling my app. This is too F*** vague and hard!
Luckily for all of us, I have really good news. Help is just around the corner and its name is “why-did-you-update”. “Why did you update” is a library that monkey patches React and notifies you in the console when potentially unnecessary re-renders occur. I really urge you to check it out on github and I promise you that you won’t be disappointed. Also, you’re welcome to print this amazing performance cheat sheet I did with the key takeaways of this post. Cheers.