Wednesday, 6 May, 2020 UTC


Summary

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, and maybe even participating in the New Relic Mobile React Native Agent beta program, you might be interested in learning how to measure component load time.
Recently, I became interested in seeing how the New Relic React Native agent could be used to measure component load time on the app, among other metrics, such as:
  • App launch count
  • User session time
  • User interaction (interaction here means the user path/trail, not gestures)
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.
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.

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.
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 the performance metrics of the class components

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:

Measure performance metrics of the functional components

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.

1.    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});

});

2.  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);

};

4.    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.
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.