React Native is compelling from a performance standpoint, but as of now the framework does not give developers a solid way to measure the speed of the apps they’re testing. As a consequence, React Native code needs to be optimized on a case-by-case basis.

And if you’re developing apps with React Native, you might be interested in learning how to measure component load time.

What is component load time?

Component load time is the time required to load all of the components (Javascript, images, etc) that complete a given screen, and it’s a good way to benchmark responsiveness and performance. It’s also not a traditional metric to measure for native applications. So if you’d like to measure it, there’s a bit of work involved.

Recently, I became interested in seeing how the New Relic React Native agent could be used to measure component load time on the app, I also looked into measuring other basic metrics, such as:

  • App launch count
  • User session time
  • User interaction (interaction here means the user path/trail)

In this post, I’ll walk through how you can measure component load time (in addition to other basic performance metrics) in a React Native app using custom instrumentation. Note that we’ll first start with class components, and then move on to functional components.

Once you can measure the performance of your app, you can take that vital information to make improvements to your app, speeding it up where possible and working out any bugs will improve your overall customer experience.

So, let’s get into it.

NEW RELIC REACT INTEGRATION
react logo
Start monitoring your React data today.
Install the React quickstart Install the React quickstart

Getting started

I began by poking around on the subject. I needed a React Native test app to try out ideas and troubleshooting. After some searching and testing, I finally ended up with this example. It was updated in the last six months and I was able to get it to work in my Xcode simulator with minimum updates.

For installation and configuration of the New Relic React Native agent, see the documentation.

Finding component load time

From there, I kicked off the project by tackling component load time.

After poking at different solutions with my team, we quickly realized our best option was recordBreadcrumb(). Since we were beginning with no auto-instrumentation, this meant I would need to identify the proper places to insert getTime() and recordBreadcrumb() calls in all levels of nested components.

Measuring class components performance metrics

With the test app ready, my next step was to determine how to know when a given component was at what stage in order to start the timer, stop the timer, calculate the duration, and transmit the results with additional attributes. This would ensure that I could build out widgets and dashboards in New Relic.

1. Capturing class component load time

While reading through React documentation, I discovered the class component lifecycle API and confirmed the approach with our Mobile APM engineers.

When a class component is first loaded, it would go through the following mounting cycle:

So by inserting a start timer in the componentWillMount() and then calculating the duration in the componentDidMount(), followed by calling the recordBreadcrumb() to push the event with all the attributes needed—such as userID, screen, component, cycle, duration, and more—to dashboard’s’ MobileBreadcrumb event type, we could capture the component mount load time:

componentWillMount() {

  startTimeM = new Date().getTime()

}

componentDidMount() {

  durationM = new Date().getTime() - startTimeM

NewRelic.recordBreadcrumb("container",{"user":"mike","screen":"explore","component":"explore","cycle":"mount","duration": durationM,"userAction":"true"});

Similarly, in the update cycle, the component would go through this cycle, and we could send a similar event including the update load time:

componentWillUpdate() {

  startTimeU = new Date().getTime()

}

componentDidUpdate() {

  durationU = new Date().getTime() - startTimeU

  NewRelic.recordBreadcrumb("container", {"user":"mike","screen":"explore","component":"explore","cycle":"update","duration": durationU,"userAction":"true"});

}

Since the parent class component load time did not include the child component load time, the relationship and hierarchy of the components were important in the recordBreadcrumb() call attributes so we could properly aggregate the child components to determine the parent component load time.

2.    Recording class component app launch count

The app launch count could be accomplished by making a recordBreadcrumb() call when the appLaunch attribute has a "true" value in the componentDidMount() in the default class or in the index.js before the AppRegistry.registerComponent() call. Then a simple NRQL query could produce the count with appLaunch='true' for a given period of time.

componentDidMount() {



  NewRelic.recordBreadcrumb("agentStart", {"user":"mike","screen":"app","component":"app","cycle":"mount", "duration": 0, "appState": AppState.currentState, "appLaunch":"true"});

3.   Calculating class component user session time

For demonstration purposes, I chose to define user session by our Mobile APM agent specs, which define a session as beginning from the point where the app is active (foreground) to ending when the app is in background. Note: the React Native agent has defined sessions as 30 minutes long with the option to set custom session length through the continueSession API.  This could be accomplished by leveraging AppState, as well as its change EventListener and setState.

Basically, when the app first mounts, the AppState EventListener is added in to listen for the change and when a session timer has started. Then, using the AppStateChange handler function and the next state info, determine whether to calculate the session duration (active to background) or reset the timer (background to active).

state = {



  appState: AppState.currentState,



};



componentDidMount() {



  startTimeA = new Date().getTime();



  AppState.addEventListener('change', this._handleAppStateChange);



};



_handleAppStateChange = (nextAppState) => {



  if (this.state.appState.match(/inactive|background/) && nextAppState === 'active') {



    startTimeA = new Date().getTime();



    NewRelic.recordBreadcrumb("appStart", {"user":"mike","screen":"app","component":"app","cycle":"session","duration": 0, "appState": AppState.currentState});



  } else if (this.state.appState.match(/inactive|active/) && nextAppState === 'background') {



    durationA = new Date().getTime() - startTimeA;



    NewRelic.recordBreadcrumb("appStop", {"user":"mike","screen":"app","component":"app","cycle":"session","duration": durationA, "appState": AppState.currentState});



    }



    this.setState({appState: nextAppState });



};

4.   Capturing class component user interaction

The user trail of events was accomplished by setting the userAction attribute to 'true' in the recordBreadcrumb() calls that were placed in the designated "checkpoint" components.

These were the components representing the area and the level/depth in the component hierarchy structure of interests. This was recommended to maintain a manageable and informative series of events that show where the user went to until the session ended.

componentDidUpdate() {



  NewRelic.recordBreadcrumb("container", {"user":"mike","screen":"explore","component":"explore","cycle":"update","duration": durationU,"userAction":"true"});



}

Note:  The componentDidUpdate() would only execute if there was an update (a render happened). If the user simply navigated back and forth between two screens, there was no “official” change or action that happened, meaning this behavior would not trigger the componentDidUpdate(). Hence, the event would not be captured within its corresponding dashboard.

Here are the results in the dashboard:

Measuring functional component performance

Now if I were to shift from class components to function components within my test app, would the agent be able to capture the same performance metrics as above?

What are the new challenges with functional components?

Moving from class components to functional components means the class component lifecycle API would no longer be available, nor could I leverage states. In addition, I would need a new test app to test ideas and demo the results.

After more searching on the web and finding nothing, it was time for me to create one from scratch. Below are some screenshots from the test app:

From the home/default screen, users can navigate via the navigation tab at the bottom of the Settings screen. From either Home or Settings, a user can navigate to the "Go to Orders" screen, which is a child component, to add/remove and order the number of SR-71 to their hearts' desire. All of this only used functional components.

Capturing functional component load time

For this I used the useEffect() hook to accomplish capturing load time. This call needs to be applied to the Home, Settings, and Details screens. The differences between the first useEffect() and the second useEffect() is that the former will only execute for the first time (mount). The latter will execute every time:

startTimeA = new Date().getTime();



//For initial mount



useEffect(() => {



  durationA = new Date().getTime() - startTimeA;



NewRelic.recordBreadcrumb("Function",{"user":"Bryan","screen":"Home", "component":"Home","cycle":"mount","duration":durationA});



},[]);



//For updates



useEffect(() => {



  durationA = new Date().getTime() - startTimeA;



NewRelic.recordBreadcrumb("Function",{"user":"Bryan","screen":"Home", "component":"Home","cycle":"update","duration":durationA});



});

Recoding functional component AppLaunch Count

Then, I moved the appLaunch recordBreadcrumb() call to the index.js.:

NewRelic.recordBreadcrumb("agentStart", {"user":"Bryan","screen":"app","component":"app","cycle":"mount", "duration": 0, "appState": AppState.currentState, "appLaunch":"true"});

3.    Calculating functional component user session time

From there, I replaced state with useState() hook. Then I continued to leverage AppState and the change event listener to accomplish the session duration capture:

const [state, setState] = useState(AppState.currentState);



//set the start timer when mount



useEffect(() => {



  startTimeA = new Date().getTime();



  NewRelic.recordBreadcrumb("appStart", {"user":"Bryan","screen":"app","component":"app","cycle":"session","duration": 0, "appState": AppState.currentState});



},[]);



//Listen to "state" change events



useEffect(() => {



  AppState.addEventListener('change', _handleAppStateChange);



},[state]);



_handleAppStateChange = (nextAppState) => {



if (state.match(/inactive|background/) && nextAppState === 'active') {



  startTimeA = new Date().getTime();



  NewRelic.recordBreadcrumb("appStart", {"user":"Bryan","screen":"app","component":"app","cycle":"session","duration": 0, "appState": AppState.currentState});



} else if (state.match(/inactive|active/) && nextAppState === 'background') {



  durationA = new Date().getTime() - startTimeA;



  NewRelic.recordBreadcrumb("appStop", {"user":"Bryan","screen":"app","component":"app","cycle":"session","duration": durationA, "appState": AppState.currentState});



}



setState(nextAppState);



};

Capturing functional component user interaction

For user interaction, I leveraged the navigation route name and state to capture where the user had gone to:

First get the current screen name:

// gets the current screen from navigation state



function getActiveRouteName(navigationState) {



  if (!navigationState) {



    return null;



}



const route = navigationState.routes[navigationState.index];



// dive into nested navigators



if (route.routes) {



  return getActiveRouteName(route);



}



  return route.routeName;



}

Then in the return() of the default app, from onNavigationStateChange(), I made a recordBreadcrumb() call if the previous screen was different from the current screen.

return <AppContainer



onNavigationStateChange={(prevState, currentState, action) => {



  const currentScreen = getActiveRouteName(currentState);



  const prevScreen = getActiveRouteName(prevState);



  if (prevScreen !== currentScreen) {



  // the line below uses call the breadcrumb api to establish user trail



  NewRelic.recordBreadcrumb("navigation", {"user":"Bryan","screen":currentScreen,"screenPrev":prevScreen,"cycle":"navigate","userAction":"true"});



  }



}}



/>;

The dashboard showed that we were able to capture the same data in a functional component-only React Native mobile app, too.

The Results

After reviewing the results, my team and I found the results of this project to be positive. With some forethought and planning, even with just the recordBreadcrumb() instrumentation, I was able to accomplish many of the basic performance metrics that were critical for today’s React Native developer.

Hopefully this article helps you understand better how to measure and improve your react native app performance.

From end user to infrastructure, our React Native Mobile Agent gives you a unique perspective across your entire system. Sign up for beta access now.