Back to Blog Home

Tips for Optimizing React Native Application Performance - Part 2: Using Sentry SDK for Performance Monitoring

Aman Mittal image

Aman Mittal -

Tips for Optimizing React Native Application Performance - Part 2: Using Sentry SDK for Performance Monitoring

Monitoring performance in front-end applications is vital. It focuses on the areas of the application users experience. These areas are slow rendering or frame rate, network request errors, and unresponsive user experience. Your application's users expect the experience of using the application to be fast, responsive, and smooth.

In the first article of this series, we discussed some tips for optimizing your React Native application performance. In this part (2), we're going to explore how to monitor performance issues in your React Native application's codebase.

In this tutorial, let's learn how to integrate Sentry SDK in a React Native application and enable React Native performance monitoring.

Pre-requisites

This tutorial requires that you are familiar with the fundamentals of React Native and JavaScript in general. If you don't already have it setup, please follow these instructions to set up a React Native environment on your local machine.

Getting Started with Sentry

Begin by creating an account, and soon after, click the Create project button.

Next, select the platform and framework you want to use. In this case, we are using a mobile platform and React Native.

The last step is to give your Sentry project a name. Once you're done, click the Create Project button.

Creating a React Native app

Open up a terminal window, and let's create a new React Native application:

npx react-native init rnBooksStore cd rnBooksStore

Your application has to use a routing library to enable routing instrumentation later. For this app, let's install the react-navigation library:

yarn add @react-navigation/native @react-navigation/native-stack react-native-screens react-native-safe-area-context

After installing the React Navigation library, make sure to follow instructions from its official documentation for linking the library for both iOS and Android.

Creating screens

In this section, let's create a mini book store. The first screen will be the Home screen. It renders a large list of books coming from the API endpoint. To illustrate the slow frame rate, the screen component uses ScrollView instead of FlatList.

I'll be using a long list of book titles from a mock API endpoint: https://example-data.draftbit.com/books?_limit=400.

Create an src/screens/ directory and inside it, create a HomeScreen.js file with the following code:

import React, { useState, useEffect } from 'react'; import { View, StyleSheet, ActivityIndicator, ScrollView } from 'react-native'; import ScreenHeader from '../components/ScreenHeader'; import BookCard from '../components/BookCard'; const HomeScreen = () => { const [books, setBooks] = useState([]); const [cart, setCart] = useState([]); const [loading, setLoading] = React.useState(true); const addToCart = book => { setCart(cart => [...cart, book]); }; const fetchBooks = async () => { try { const response = await fetch( 'https://example-data.draftbit.com/books?_limit=400' ); const json = await response.json(); setBooks(json); } catch (error) { console.error(error); } finally { setLoading(false); } }; useEffect(() => { fetchBooks(); }, []); if (loading) { return ( <View style={styles.loadingContainer}> <ActivityIndicator size="large" color="#0000ff" animated /> </View> ); } else { return ( <View style={styles.container}> <ScrollView contentContainerStyle={styles.listContainer}> <ScreenHeader cart={cart} /> {books.map(book => ( <BookCard key={book.id.toString()} thumbnailUrl={book.image_url} title={book.title} authors={book.authors} shortDescription={book.description} addToCart={addToCart} /> ))} </ScrollView> </View> ); } }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', paddingHorizontal: 10, }, loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', }, listContainer: { padding: 10, }, text: { fontSize: 24, fontWeight: '600', }, headerCartContainer: { justifyContent: 'space-between', flexDirection: 'row', padding: 10, }, }); export default HomeScreen;

Creating custom components

The HomeScreen component uses two custom components. The first one displays the count of books when added to the cart. This component is called ScreenHeader. Create a src/components/ScreenHeader.js file with the following code:

import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; const ScreenHeader = ({ cart }) => { return ( <View style={styles.container}> <Text>Cart Items</Text> <Text style={[ { fontSize: 20 }, !cart.length ? { color: 'gray' } : { color: '#0000ff' }, ]} > {cart.length} </Text> </View> ); }; const styles = StyleSheet.create({ container: { justifyContent: 'flex-end', alignItems: 'flex-end', marginLeft: 10, }, }); export default ScreenHeader;

The second component renders the info of each book item. Create a src/components/BookCard.js file with the following code:

import React from 'react'; import { View, Image, TouchableOpacity, Text, StyleSheet } from 'react-native'; const BookCard = ({ thumbnailUrl, title, authors, shortDescription, addToCart, }) => { return ( <View style={styles.container}> <View style={styles.flex}> <Image source={{ uri: thumbnailUrl }} style={styles.image} /> </View> <View style={styles.itemInfoContainer}> <Text style={styles.title}>{title}</Text> <Text style={styles.author}>by {authors}</Text> <Text style={styles.description} numberOfLines={2}> {shortDescription} </Text> {addToCart && ( <TouchableOpacity onPress={() => addToCart({ title })}> <View style={styles.buttonContainer}> <Text style={styles.buttonText}>Add To Cart</Text> </View> </TouchableOpacity> )} </View> </View> ); }; const styles = StyleSheet.create({ container: { flexDirection: 'row', marginVertical: 10, paddingHorizontal: 10, }, flex: { flex: 1, }, image: { height: 125, resizeMode: 'contain', }, itemInfoContainer: { flex: 3, paddingHorizontal: 10, justifyContent: 'space-between', }, title: { fontWeight: 'bold', fontSize: 14, }, author: { color: 'gray', fontStyle: 'italic', }, description: { color: 'gray', fontSize: 12, }, buttonText: { color: 'white', marginHorizontal: 4, }, buttonContainer: { flexDirection: 'row', flexWrap: 'wrap', borderRadius: 12, padding: 8, alignSelf: 'flex-start', backgroundColor: '#0000ff', alignItems: 'center', }, }); export default BookCard;

Adding a Home stack navigator

Although we only have one screen in the example application, let's add a HomeStack navigator so we can learn how to create routing instrumentation configuration.

Create a src/navigation/HomeStack.js file with the following code:

import * as React from 'react'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import HomeScreen from '../screens/HomeScreen'; const Stack = createNativeStackNavigator(); const HomeStack = () => { return ( <Stack.Navigator> <Stack.Screen name="Home" component={HomeScreen} /> </Stack.Navigator> ); }; export default HomeStack;

Integrating Sentry SDK in React Native app

Sentry's SDK for React Native enables automatic crash reporting and performance monitoring. It captures these issues using the SDK within your application’s runtime.

Open up your terminal window to install and set up Sentry's React Native SDK:

yarn add @sentry/react-native yarn sentry-wizard -i reactNative -p ios android npx pod-install

The sentry-wizard takes care of initializing the Sentry SDK in the React Native App.{js|tsx} file and linking the SDK for both iOS and Android platforms. (Note: the automatic linking only works if your React Native project uses version 0.60 and above.)

By default, the Sentry SDK initialization adds a unique Data Source Name (DSN) string that monitors events in your application.

I'll modify the Sentry SDK initialization to add a tracesSampleRate property with a value of 1.0. This will enable the tracking of all events in the application. However, in production, ensure that the value of tracesSampleRate is lower than 1.0 to collect a uniform amount of sample data without reaching Sentry's transaction quota. You can learn more about sampling in Sentry's official documentation.

Here is an example of Sentry initialization of the App.js file:

import React, { useRef } from 'react'; import { NavigationContainer } from '@react-navigation/native'; import * as Sentry from '@sentry/react-native'; import HomeStack from './src/navigation'; Sentry.init({ dsn: 'https://da5d189961d145fa91ce878346cce14c@o1267720.ingest.sentry.io/6461490', tracesSampleRate: 1.0, }); const App = () => { return ( <NavigationContainer> <HomeStack /> </NavigationContainer> ); }; export default Sentry.wrap(App);

Wrapping the root component of the React Native app like Sentry.wrap() enables automatic performance monitoring. You can also use another high-order component called Sentry.withProfiler() to wrap the root component. It enables tracking the component's lifecycle as a child span of the route transaction.

Sentry.withProfiler(App);

Enabling routing instrumentation

Routing instrumentation in Sentry's SDK supports the latest version of React's Navigation library. The routing instrumentation creates a transaction on every route change. So let's first add the configuration to enable the routing instrumentation.

In the App.js file, create a navigation reference. It connects to the Navigation container. The onReady() method on NavigationContainer will register the navigation container with the routing instrumentation.
Here is what the App.js file looks like after modification:

import React, { useRef } from 'react'; import { NavigationContainer } from '@react-navigation/native'; import * as Sentry from '@sentry/react-native'; import HomeStack from './src/navigation'; const routingInstrumentation = new Sentry.ReactNavigationInstrumentation(); Sentry.init({ dsn: 'https://your-dsn-string', integrations: [ new Sentry.ReactNativeTracing({ routingInstrumentation, // ... other options }), ], tracesSampleRate: 1.0, enableAutoSessionTracking: true, // For testing, session close when 5 seconds (instead of the default 30) in the background. }); const App = () => { const navigation = useRef(); return ( <NavigationContainer ref={navigation} onReady={() => { routingInstrumentation.registerNavigationContainer(navigation); }} > <HomeStack /> </NavigationContainer> ); }; export default Sentry.withProfiler(App);

Performance Monitoring in action

Open two terminal windows. In the first window, initiate the React Native packager. In the second one, build the application for either iOS or Android. I'll be using the iOS simulator to run the application.

# first terminal window npx react-native start
# second terminal window # for iOS npx react-native run-ios # for android npx react-native run-android

After the application is built, you will get the following result:

Now, if you head back to Sentry's dashboard and navigate to the Performance tab from the sidebar, you will notice it has started to monitor events in our React Native app.

You can click the Mobile tab for the performance overview. It contains information about slow and frozen frames, as well as app start.

In the screenshot below, you can see the app has started to monitor App start and Home event:

The Home is the Home screen; since we have routing instrumentation enabled, Sentry will pick up and display the exact screen name.
If you click the transaction, you'll get additional details about what is happening. For example, it will tell you that the starting time of the app is warm from the event app.start.warm.

On iOS devices, Apple recommends that your app should take a maximum of 400ms to render the first frame. However, our start-up time was a little over 450ms as seen in the screenshot.

Fetching data from the HTTP request (http.client) also takes a little less than one second. Increasing the number of books coming from the API endpoint will increase the start-up time. Thus, large lists of data using a FlatList component can significantly improve the start-up time. Also, pre-fetching data from the API endpoint using a library like react-query can also help improve the start-up time on app restarts.

We can also optimize the BookCard component by caching the image or using a useCallback hook to stop re-rendering the parent component each time a book is added to the cart.

By clicking on “Suspect Spans”, you can view all the transactions in the application:

Conclusion

Using a powerful tool like Sentry allows you visibilty into your users' experiences and provides an understanding of how you can improve it. In this article, you set up and integrated the Sentry SDK in a React Native application and learned how to monitor the application for performance with routing (navigation) enabled and network calls. Mobile application monitoring is a crucial aspect to engage app users and provide a seamless experience iteratively. It is only possible when these insights are available to us.

Share

Share on Twitter
Share on Bluesky
Share on HackerNews
Share on LinkedIn

Published

Sentry Sign Up CTA

Code breaks, fix it faster

Sign up for Sentry and monitor your application in minutes.

Try Sentry Free

Topics

Guest Posts

New product releases and exclusive demos

Listen to the Syntax Podcast

Of course we sponsor a developer podcast. Check it out on your favorite listening platform.

Listen To Syntax
© 2024 • Sentry is a registered Trademark of Functional Software, Inc.