How We Boosted the Performance of Our React Native App

Juanjo Ramos
In The Hudl
Published in
7 min readSep 14, 2018
Cuentos y Fábulas

This post discusses the work done our React Native application to improve UI responsiveness. It doesn’t aim to be a list of best practices you should follow to improve your own app’s performance, but some of our improvements did come from following those practices.

We were a team of three developers, all with a strong iOS background but only one with previous React Native experience. We continued the work of an existing React Native application where the user could see a live video feed featuring moments¹.

Soon after we started working on the app, we noticed something wasn’t quite right. The performance of the app was significantly worse than what we’d seen in native projects before. In particular we noticed that:

  1. There was a significant lag (0.5s+) between tapping any UI control and the execution of that action.
  2. That lag experienced linear growth with respect to the length of the video session and the number of moments.

To be precise, after 20+ minutes of use, the Head of Elite Support at Hudl would rate the app’s responsiveness with:

La Comiquera

¹ A moment is a region of interest within a video, e.g., if a video corresponds to a basketball game, one moment could be the tip-off.

Metrics to Assess UI Responsiveness

We had to make it obvious to our product owner that the responsiveness of the application had improved. More importantly, we had to prove that it wouldn’t degrade over time.

At the time of solving this problem we were focused only on Windows. The fact that this platform isn’t officially supported by React Native comes with its own set of complexities. One of the biggest is that the Performance Monitor doesn’t work on UWP (at least not in React Native 0.47.2, which was the version we used).

Our first attempt featured Visual Studio profiling tools. They provided some useful data like CPU spikes when a new moment appeared in the application, but we couldn’t find any reliable and quantifiable metric that would correlate well to performance and responsiveness.

Snapshot from Visual Studio Diagnostic Tools

Luckily, Hudl has quite a few React and React Native experts² we could rely on, and they suggested looking at the number of render counts per component. We ended up with the following process:

  1. Create a document with different workflows and the number of expected renders per component.
  2. Create a script to count every time render() is called in a component, and write those counts to a local html file.
  3. Compare 1 and 2.

We also asked our product owner and a member of our support team to rate the responsiveness of the application before and after our work. Although this metric doesn’t ensure responsiveness won’t degrade over time, it does validate that the quantifiable metric correlates well with responsiveness.

So we ended up with two key metrics:

  1. Each component’s render() count, checking that the actual values match what’s expected for a pre-defined set of workflows.
  2. A pre- and post-work assessment by one member of Elite Support and our product owner.

² Mihai Cîrlănaru is one of the React experts at Hudl. Check out his presentation about React Performance Optimizations.

Improving UI Responsiveness

As indicated earlier, the React Native experience wasn’t great when we joined the project. We first had to arm ourselves with knowledge about component lifecycle and behaviour.

1. A React component re-renders when a prop or state changes.

2. In a PureComponent, a “change” is found during a shallow comparison of previous and current props and states.

3. In a Component, “change” means any difference in a reference or value between previous and current props and states.

4. Implementing shouldComponentUpdate() provides fine control on whether or not the component should re-render.

5. shouldComponentUpdate() can only be overridden in Component, not in PureComponent.

With that information we did different iterations to remove unnecessary renders in our code.

Iteration 1: Avoid Passing Inline Functions as Props

BEFORE
AFTER

There is one small difference that could be missed with a quick read: How the onPress prop is passed.

In the first example, a new reference to that inline function is declared in every render of DrawerTitle. Thus, DrawerComponent will always re-render itself because one of its props will always change.

In the second example, the reference to that function is always the same and therefore DrawerComponent will not re-render (unless any of the other props change).

My colleague Jon Reynolds published a more detailed explanation of this problem a few months ago.

Iteration 2: Using PureComponent & shouldComponentUpdate()

Once applied, we did see some performance gains. The expected and actual render cycles for a number of components began to match up. However, there were still some problems.

  1. We were using these functionalities as a silver bullet.
  2. For some components, the expected and actual number of renders for a given workflow were still way off.
  3. The longer we ran the application, the worse the performance would be (and the greater the number of renders). So, after about 20 minutes we were like:
tenor.com

It was clear that the problem was somewhere else. Those PureComponents were still re-rendering, so the question became why were the parents re-rendering in the first place? We took a step back to analyse the full picture and move to the last iteration.

Iteration 3: Refactor Your Code

This is a diagram of the component’s hierarchy in our application.

Our application was suffering from what we called props infection. There was a prop value that changed every second, propagated to pretty much all components in that diagram.

The biggest impact came from that MomentList on the right — it could contain between 200 and 1,000 MomentListItems. If you remember: the longer the session, the worse the responsiveness. And the longer the session, the more MomentListItem on that list, making every render() more expensive.

That “viral prop” was expressing video duration for the native video player. Because this application plays live video, that value would change approximately every second.

The work in this iteration was simple:

  1. Remove that duration prop from any component not using it (there were a few).
  2. Refactor some components to receive only the props they would need to render themselves, e.g., a MomentListItem does not need the duration to render itself.

Results: Metrics Before and After

Counting render() calls

After all the refactoring work, the expected and actual render() calls on the components of interest did match.

Performance Measured by Elite Support and Product Owner

Before the project, they had rated the app’s performance as a 1/5 and a 2/5, respectively. Both gave a 5/5 once we were done.

Conclusions

We learned a number of lessons while improving the performance of our application.

  1. PureComponent and shouldComponentUpdate() are good tools for any toolset. Know when to use them.
  2. Make sure you pass only the props the component needs. In particular, if it’s a presentational component, make sure it only receives the props it needs to render itself.
  3. Leverage the redux store or other similar mechanisms when appropriate, e.g., don’t pass a prop through three components to reach a child when that child can get it directly from the redux store.
  4. If you find yourself using Component + shouldComponentUpdate() too often, it might be a code smell. Ask yourself if it makes sense that the parent component is asking this component to render in the first place.
  5. There’s no rule of thumb. With great power comes great responsibility and Component, PureComponent and Functional Component give you great power. Defaulting to PureComponent might be the de-facto standard, but be sure to weigh your options. There might be a better fit for your situation.

A Few Caveats

  • PureComponent internal implementation of shouldComponentUpdate() may lead to false-negatives when your object contains complex data structures. From React’s Documentation:

If these contain complex data structures, it may produce false-negatives for deeper differences. Only extend PureComponent when you expect to have simple props and state, or use forceUpdate() when you know deep data structures have changed. Or, consider using immutable objects to facilitate fast comparisons of nested data.

  • shouldComponentUpdate() implementation should be simple. You may run into situations where the cost of executing the function is greater than just re-rendering the component.
  • A Functional Component will always update if the parent component updates.

--

--

Juanjo Ramos
In The Hudl

Engineer Manager at Hudl. I do like iOS and macOS development but I like even more working with people and make people around me better.