Back

Performance Tips for React Native Apps

Performance Tips for React Native Apps

Performance is a critical consideration when developing web pages and applications. A fast, responsive application can provide a better user experience, increasing user satisfaction and engagement. On the other hand, slow loading times or unresponsive behavior can frustrate users and lead to decreased usage or abandonment. Therefore, it is essential to carefully consider performance when building webpages and applications to ensure a smooth, seamless user experience.

As a developer, your React Native needs to be fast and fully optimized, depending on how you write your code. This article will cover techniques and tips to optimize React Native apps for higher performance. Buckle up and get ready to boost them like a pro

Reasons for a Slow React Native App

A React Native app may be underperforming for several reasons. The following are some typical reasons why React Native apps operate slowly:

  • Complex UI. An app with a complex UI with many nested components takes longer to render, leading to poor performance.
  • Unnecessary re-renders cause the app to slow down. This can be caused by excessively using the setState method or not using the shouldComponentUpdate lifecycle method. Components that re-render unnecessarily or perform expensive calculations on each render cause application performance overheads.
  • Large component hierarchies: Poorly designed component architecture that has a large number of children, it takes longer for the component to render.
  • A large amount of data takes longer to render, leading to poor performance, especially if the component renders a large dataset inefficiently.
  • Complex animations. Animations that are implemented poorly or that are too complex slow down component rendering.
  • Memory leaks occur when an app uses more and more memory over time, even when the application does not need an operation at a given time. Memory leaks keep the operation holding memory, and the application expects such operation to release the assigned memory and assign it to the subsequent operations. Lack of such memory reallocation leads to memory leakage.
  • Complex calculations performing complex calculations takes longer for the component to render.
  • Lack of optimization. Your components need to be optimized for performance, and the lack of such practices is caused by using the wrong components or not using performance optimization techniques such as memoization.

Identifying such root causes of the slow performance is the first step to jumping in and finding the best solutions to address the elephant in the room. The following sections will discuss the common things that you need to do and improve the performance of your React Native app.

CSS Animations Instead of JS Animations

Animations play a significant role in enhancing the user experience of an app. There are various ways to implement animations in React Native applications, including CSS and JavaScript-based animations. Choosing the proper animation technique helps create a performant and more enjoyable user experience for the app.

JavaScript-based animations rely on JavaScript runtime. Using CSS animations instead of JavaScript-based animations potentially improves the performance of a React Native app. This is because the browser’s rendering engine handles CSS animations. This engine is optimized for performance out of the box for animation performance and frame rate.

CSS animations allow you to specify a series of keyframes. These are points in the animation process that define the appearance of an element. Each keyframe uses CSS to specify the element’s appearance at a specific animation stage. The browser then creates the animation by transitioning from one keyframe to the next. Check out this basic component example that uses CSS animations:

import React, { useRef } from 'react';
import { View, Text, StyleSheet } from 'react-native';

function EXComponent() {
  const elementRef = useRef(null);

 function startAnimation() {   
   elementRef.current.classList.add('animation');
  }

  return (
    <View style={styles.container}>
      <Text ref={elementRef} style={styles.text}>Hello World</Text>
      <Button title="Start Animation" onPress={startAnimation} />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justify-content: 'center',
  },
  text: {
    fontSize: 20,
  },

  'animation': {
    animation: 'fadeIn 1s',
  },
  '@keyframes fadeIn': {
    from: {
      opacity: 0,
    },
    to: {
      opacity: 1,
    },
  },
});

export default EXComponent;

FlatList Instead ScrollView

Rendering large data sets is part of application development. However, the approach you choose to load data to your application determines the overall performance of a given component. In React Native, you display a data list in two significant ways: FlatList and ScrollView.

Each approach has its underlying performance to the component used for rendering these data sets.

The FlatList uses a ‘virtualized’ rendering technique. This means that it only renders the currently visible items on the screen. It reduces the number of elements that need to be rendered and managed by the app based on the user’s current screen in relation to the position of an item in that data list.

import React from 'react';
import { FlatList, Text, View } from 'react-native';

const data = [
  { id: '1', title: 'Test Item', description: 'This is the description'},
  { id: '2', title: 'Test Item', description: 'This is the description'},
  { id: '3', title: 'Test Item', description: 'This is the description'},
];

function MyList() {
  return (
    <FlatList
      data={data}
      renderItem={({ item }) => (
        <View>
          <Text>{item.title}</Text>
          <Text>{item.description}</Text>
        </View>
      )}
      keyExtractor={item => item.id}
    />
  );
}

export default MyList;

Here keyExtractor prop is used to specify a unique key for each item in the list that will be displayed on the current user screen.

ScrollView is still desirable for rendering lists of data. However, it works fine for small lists of items. It renders all the items in the list, which is less efficient when rendering large lists.

import React from 'react';
import { ScrollView, Text, View } from 'react-native';

function MyList({ data }) {
  return (
    <View>
      <ScrollView>
         /* rendering a small list of items */
        {data.slice(0, 10).map(item => (
          <View key={item.id}>
            <Text>{item.title}</Text>
           
<Text>{item.description}</Text>
          </View>
        ))}
      </ScrollView>
    </View>
  );
}

export default MyList;

FlatList is suited when rendering long lists of data. It only renders the items currently visible on the screen and removes items from the rendering tree when they are no longer visible to minimize memory usage. As a result, using a FlatList component makes your React Native app faster and more efficient.

Other techniques that come in handle include using infinite scrolling and implementing paginations to your lists of data.

Memoize React Components

Memoization optimizes the performance of a function by storing the results of expensive function calls and returning the stored results when the same inputs occur again. It helps you avoid recalculating the results each time the function is called.

In the context of React Native, memoization is used to optimize the performance of components by avoiding unnecessary re-renders. When a component is memoized, its output is cached, and the component will only re-render if its inputs (props) change. This stores and reuses expensive rendering components, such as complex calculations and DOM manipulation.

React Native uses React.memo() to memoize a component. Below is a basic example of a memoized React Native component using React.memo() that will memoize and only re-render if the Text props changes:

import React from 'react';
function TestComponent(props) {
  
  return <Text>{props.text}</Text>;
}
export default React.memo(TestComponent);

React.memo() is a higher-order component similar to useMemo. It is used for function components instead of hook functions. You can use the useMemo and useCallback hooks to memoize expensive calculations and functions. Here is a basic example:

import { useMemo, useCallback } from 'react';
function TestComponent(props) {

  // useMemo
  const expensiveValue = useMemo(() => {
  }, [props.a, props.b]);

  // useCallback
  const expensiveFunction = useCallback(() => {
  }, [props.a, props.b]);

  return (
    <div>
      {expensiveValue}
      <button onClick={expensiveFunction}>Click me</button>
    </div>
  );
}

Memoization can increase the complexity of your code. Use memoization when it is necessary and beneficial for your specific use case.

Session Replay for Developers

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — an open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

Use Hermes

Hermes is a JavaScript engine developed by Facebook. It is specifically designed to improve the performance of React Native applications on Android devices. However, it doesn’t run by default. To enable it in your React Native project, follow these steps: Make sure you use React Native version 0.62 or higher.

  • In your android/app/build.gradle file, add the following line to the defaultConfig block:
defaultConfig {
ndk.abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
}
  • In the android/app/src/main/AndroidManifest.xml file, add the following to the application element:
<application android:usesCleartextTraffic="true">
</application>
  • Navigate to android/app/src/main/java/com/[your-app-name]/MainApplication.java file, and add the following import:
import com.facebook.hermes.reactexecutor.HermesExecutorFactory;
  • In the same file, add the following lines to the getPackages method:
@Override
protected List<ReactPackage> getPackages() {
    return Arrays.<ReactPackage>asList(
        new MainReactPackage(),
        new MyCustomReactPackage(),
        new HermesPackage()
    );
}
  • In the same file, add the following methods:
@Override
protected String getJSMainModuleName() {
    return "path/to/your/index";
}

@Override
protected ReactExecutorFactory getReactExecutorFactory() {
  return new HermesExecutorFactory();
}

Run react-native run-android to build and run your app. After enabling Hermes, you will notice improved performance in your apps at start-up time on Android devices.

Avoid Props in Initial States

Props are (or should be!) immutable. Changing them causes unexpected behavior. Avoiding props as initial state values help to ensure that your component’s internal state is self-contained and easier to understand and debug.

Instead of using props, use the useState hook to define an initial state value for a component, for example:

import { useState } from 'react';

function MyComponent(props) {
  const [value, setValue] = useState(props.initialValue);

  return (
    <div>
      <button onClick={() => setValue(value + 1)}>Increment</button>
      <p>{value}</p>
    </div>
  );
}

The useState hook defines an initial state value for the value variable. The value is initialized to the initialValue prop. This way, it can be changed by calling the setValue function. This allows the component to maintain its internal state, separate from the props it receives.

Alternatively, use constructor methods to achieve the same as follows:

class TestComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: props.initialValue,
    };
  }

  render() {
    return (
      <div>
        <button onClick={() => this.setState({ value: this.state.value + 1 })}>
          Increment
        </button>
        <p>{this.state.value}</p>
      </div>
    );
  }
}

Here you avoid using the initial state value using a constructor method. It defines an initial state value for the value variable. The value is initialized to the initialValue prop, which is changed by calling the setState function.

Escape Re-renders

Re-rendering is necessary at some points. However, there are scenarios in which re-renders are unnecessary. Let’s check out a few ways to avoid re-renders. They include (With some strategies we have already discussed here):

  • Use the React.memo higher-order component or the PureComponent base class to optimize the performance of the function and class components. These components will only re-render if their props have changed.
  • Use the useMemo hook to memoize expensive computations and avoid re-computing them on every render.
  • Use the useCallback hook to memoize callback functions and avoid creating unnecessary functions on every render.
  • Always avoid using props or state values in the render method that are not used to determine the content of the component.
  • Avoid using the forceUpdate method to trigger a re-render.

Dependency and Code Optimization

In React, it is essential to carefully consider the dependencies that you use to perform different operations. React Native uses NPM dependencies. To your application bundle size, you need to consider the amount of code each package you install adds to your application in relation to the code you are utilizing and their dependencies.

For example, consider using Moment.js. Use the moment-locales-webpack-plugin to remove unnecessary language support from your Moment.js library. This help to reduce the size of your final bundle. It is handy if you only use a single language in your app and don’t need the additional localization files that come with Moment.js. To use the plugin, you will need to install it and add it to your webpack configuration.

const MomentLocalesPlugin = require('moment-locales-webpack-plugin');

module.exports = {
  plugins: [
    // Other plugins here
    new MomentLocalesPlugin({
      localesToKeep: ['en'], 
    }),
  ],
};

Alternatively, use the lodash-webpack-plugin to remove unnecessary functions from your Lodash library and reduce the size of your final bundle. This is especially useful if you only use a small subset of the functions provided by Lodash and don’t need the rest of the library in your final bundle. To use the plugin, you will need to install it and add it to your webpack configuration as follows:

const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');

module.exports = {
  plugins: [
    new LodashModuleReplacementPlugin({
      collections: true,
      shorthands: true,
      paths: true,
    }),
  ],
};

When using console statements for debugging codes in a development phase, remove them before rolling out production versions. Use babel-plugin-transform-remove-console to remove these statements. Always remove unnecessary libraries and code, such as unused CSS code, and improve performance with CSS.

These approaches help reduce your bundle size and improve your app’s performance.

Cache and Optimize Assets

In a React Native app, optimizing the size and performance of assets like images, fonts, and other media files is important. There are a few strategies you can use to optimize these assets:

  • Image optimization tools such as imagemin to reduce the file size of images without sacrificing quality.
  • Using vector graphics instead of raster graphics. Vector graphics can be scaled to any size without losing quality.
  • Use font icons instead of image icons. Font icons are scalable and have a smaller file size.
  • Add the @2x and @3x suffixes to specify different versions of an image for different screen densities, and use the Image component’s resizeMode prop to control how the image is scaled.
  • Use the Image component’ source prop to specify a low-resolution version of an image as a placeholder while the high-resolution version is being loaded.
  • Use the ImageBackground component to apply a tiling image as a background rather than a full-size image.
  • If necessary, use SVG to create your application assets.

Using a CDN

CDNs (content delivery networks) are distributed server systems that deliver content based on the user’s location. An app served by a CDN redirects requests to the server closest to the user when a user requests content. This improves content delivery performance and speed.

CDNs are commonly used to deliver static content, such as images, fonts, and other media files. It ensures that your static assets are delivered to users as quickly as possible, regardless of location. The benefit of this is that it reduces the distance assets need to travel between the server and the client. This is useful for websites and apps with global audiences.

Your assets must be hosted on a CDN server and referenced in your React Native app’s code using the CDN URL.

<Image
  source={{
    uri: 'https://my-cdn.com/images/logo.png',
  }}
/>

Top CDN providers you can use include Amazon CloudFront, Cloudflare, Akamai, MaxCDN, Google Cloud CDN etc.

Conclusion

Optimization is an inevitable day-to-day practice for developers. Users require an application that serves them what they need excellently. As a developer, this places a demand you always need to accomplish while putting your users on the upper hand.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs and track user frustrations. Get complete visibility into your frontend with OpenReplay, the most advanced open-source session replay tool for developers.

OpenReplay