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:

Create a document with different workflows and the number of expected renders per component. Create a script to count every time render() is called in a component, and write those counts to a local html file. 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:

Each component’s render() count, checking that the actual values match what’s expected for a pre-defined set of workflows. 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.

We were using these functionalities as a silver bullet. For some components, the expected and actual number of renders for a given workflow were still way off. 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:

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: