The Talent500 Blog
Native App

Boosting React Native App Performance: Secrets Unveiled

In the rapidly evolving world of mobile app development, React Native has emerged as a game-changer. 

By harnessing the power of JavaScript and React, React Native empowers developers to build truly native apps using a familiar and versatile language by leveraging a “learn once, write anywhere” approach. 

The beauty of React Native lies in its ability to bridge the gap between native components and JavaScript code.

Sometimes you experience slow loading of components or data and laggy interactions on screens in your application. 

You’re now worried that it will ruin the user experience leading to negative reviews, lower ratings on your app, and lower retention and engagement.

But not anymore…

In this article, we embark on secrets for boosting your React Native app performance so that your app users can get the most out of it and can enjoy using your app. 

Faster apps provide a smoother and more enjoyable experience for users. 

Improving the performance of your React Native App is crucial for several reasons:

  1. User Experience (UX): Slow-loading screens and laggy interactions can frustrate users, leading to higher abandonment rates and negative reviews.
  2. Reduced Crashes and Errors: If the app crashes frequently or displays error messages, it can frustrate users and discourage them from using the app. Improving performance helps reduce these issues and enhances the app’s stability.
  3. Retention and Engagement: Users are more likely to use an app that responds quickly and smoothly regularly. High-performance apps tend to retain users better and keep them engaged.
  4. App Store/Play Store Rankings: App responsiveness, a user engagement metric is a crucial factor when determining app rankings. A well-performing app is more likely to reach a wider audience and may have better visibility.
  5. Cost Efficiency: Optimized apps consume fewer resources such as CPU, data, RAM, and Memory. Efficiency can save costs for users with limited data plans and can lead to lower server expenses for developers.

Now that we understand the importance of performance improvement and app optimization here are some key areas to focus on when optimizing a React Native app:

Ways to Improve the Performance of Your React Native App

  • React.memo() and PureComponent

`React.memo()` and  `PureComponent` is used to prevent unnecessary re-renders when the props of a component have not changed.

Both are used for performance optimizations, but they have some differences in how they work.

  • React.memo(): 
  • React.memo() is a higher-order component that you can use to wrap a functional component. 
  • It can be used in React web applications as well as it is part of the React library and is not specific to React Native.
  • When a functional component is wrapped with React.memo(), it will only re-render if the props provided to the component have changed. 
  • It performs a shallow comparison of props to determine if re-rendering is necessary. 
import React from ‘react’;

const MyComponent = React.memo((props) => {
  // Your component logic and JSX here
});

export default MyComponent;
  • PureComponent:
  • It is typically used with class components, and the class extends PureComponent instead of Component (which is the base class for regular class components).
  • Similar to React.memo(), PureComponent also performs a shallow comparison of props and state. If there are no changes in the props or state, it prevents the component from re-rendering. 
import React, { PureComponent } from ‘react’;

class MyComponent extends PureComponent {
  // Your component logic and JSX here
}

export default MyComponent;

If you are working with functional components, you can use React.memo(), while for class components, you can use PureComponent. 

However, remember that functional components with hooks are becoming the recommended approach in React Native, and class components are less common in modern days.

  • Optimize State Updates

In React Native, functional state updates are used to optimize state updates, especially when the new state relies on the previous state. 

This ensures that you always work with the most up-to-date state and avoid potential race conditions or stale data.

  • Functional State Update with Functional Components:

Use the `useState` hook to achieve the optimized state updates.

import React, { useState } from ‘react’;

const MyComponent = () => {
const [count, setCount] = useState(0);

const handleIncrement = () => {
// Functional state update using previousCount (prevCount)
setCount((prevCount) => prevCount + 1);
};

return (
<div>
<p>Count: {count}</p>
<button onClick={handleIncrement}>Increment</button>
</div>
);
};

export default MyComponent;

  • Functional State Update with Class Components:

You can use the functional form of `setState`, which takes the previous state as an argument and returns the new state based on it.

Here’s how you can use setState in a class component:

import React, { Component } from ‘react’;

class MyComponent extends Component {
state = {
count: 0,
};

handleIncrement = () => {
// Functional state update using prevState
this.setState((prevState) => ({ count: prevState.count + 1 }));
};

render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.handleIncrement}>Increment</button>
</div>
);
}
}

export default MyComponent;

  • FlatList over ScrollView

In React Native, both FlatList and ScrollView are components used for displaying lists or content that can be scrolled.

However, they serve different purposes and handle the rendering of data differently.

  • FlatList:
  • FlatList uses virtualization, which efficiently renders only the visible items on the screen and recycles off-screen items, saving memory and improving performance.
  • It takes a data prop, which is an array of data to be rendered as a list.
  • It requires a renderItem prop, which is a function that defines how to render each item in the list.

Use FlatList when: 

  • You have a large list of data to display, as it offers better performance due to its optimized rendering.
  • ScrollView:
  • ScrollView is a more generic component that allows you to scroll a single child component or a group of child components.
  • It’s not optimized for handling long lists of data as efficiently as FlatList.

Use ScrollView when you have a smaller list or a group of components that you want to scroll together.

  • Avoid Inline Functions in Render

Avoid creating new functions inside the render/return method, as it can lead to unnecessary re-renders of child components. 

Instead, define functions outside the render or return method and pass them as props.

Here is why you should not use inline functions inside render or return:

  • Function Recreations:

A new instance of that inline function is created every time the component renders.

This can lead to unnecessary function recreations, causing extra memory allocation and garbage collection overhead.

  • Component Re-renders 

If you define an inline function inside the render method, any parent component’s re-render will cause the child component (where the inline function is defined) to re-render as well.

As we know, react components re-render whenever there is a state or prop change.

This can lead to unnecessary re-renders of child components and can degrade app performance.

Avoid inline function in the render method of class component:

class MyComponent extends React.Component {
  render() {
    return (
      <TouchableOpacity onPress={() => this.handlePress()}>
        <Text>Click me</Text>
      </TouchableOpacity>
    );
  }

  handlePress() {
    // handle press logic
  }
}

Avoid inline functions in return method of functional component:

import React from ‘react’;

const MyComponent = () => {
  const handlePress = () => {
    // handle press logic
  };

  const handleInputChange = (text) => {
    // handle input change logic
  };

  return (
    <View>
      <TouchableOpacity onPress={handlePress}>
        <Text>Click me</Text>
      </TouchableOpacity>
      <TextInput onChangeText={handleInputChange} />
    </View>
  );
};

export default MyComponent;
  • Use shouldComponentUpdate() or React.memo() for Custom Components

Both `shouldComponentUpdate()` and `React.memo()` are used for optimizing performance in React components, but they serve different purposes and are applicable in different scenarios.

  • shouldComponentUpdate():
  • shouldComponentUpdate() is a lifecycle method available in class components.
  • By default, React re-renders a component whenever its parent re-renders, even if the props and state remain the same. By implementing shouldComponentUpdate(), you can add custom logic to compare the previous and current props and state and decide if a re-render is necessary.

Example of how you can use shouldComponentUpdate():

class MyComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    // Add your custom logic to compare nextProps and nextState with current props and state
    // Return true if you want the component to re-render, false otherwise
    return nextProps.someProp !== this.props.someProp || nextState.someState !== this.state.someState;
  }

  render() {
    // Render component logic here
  }
}

If you are working with class components, you can use shouldComponentUpdate() to optimize rendering behavior based on props and state changes. 

On the other hand, if you are using functional components, consider using React.memo() to automatically optimize re-renders when the component receives the same props.

  • Performance Profiling

Use the React Native Performance Monitor, Flipper, or other profiling tools to identify performance bottlenecks in your app. 

This can help you pinpoint the exact components or processes that are causing the most re-renders.

Here are some commonly used performance profiling tools in React Native: 

  • React Native Debugger: 

React Native Debugger is a standalone desktop application that allows you to debug and profile React Native applications. It provides a performance tab that allows you to record and inspect performance metrics such as FPS (Frames Per Second), CPU usage, and more. 

  • Flipper: 

Flipper is an extensible mobile app debugging tool by Facebook. It can be used with React Native and other mobile platforms. Flipper provides plugins to profile the performance of your React Native app, inspect the state, and analyze network traffic.

  • Systrace: 

Systrace is a built-in Android tool for profiling app performance. It provides a detailed trace of system-level events, such as CPU usage, memory, and rendering, which can help you identify performance bottlenecks. 

  • Xcode Instruments: 

For iOS development, Xcode Instruments is a powerful tool for profiling and optimizing React Native applications. It allows you to measure CPU usage, memory allocation, energy impact, and more. 

To use these performance profiling tools, follow these general steps: 

Set up the Tool: Install the performance profiling tool you want to use. Some tools require specific configurations to work with your React Native project. 

Run Your App: Start your React Native app either on a simulator/emulator or a physical device. 

Connect the Profiling Tool: Connect the tool to your running app. The process for connecting varies based on the tool you’re using. 

Record and Analyze: Once connected, start recording performance data. Perform actions within your app that you want to profile. 

After recording, analyze the results to identify performance bottlenecks and areas for improvement.

  • Optimize Images

Use compressed and appropriately sized images for different screen resolutions to reduce the loading time and memory usage of images in your app.

Optimizing images in a React Native app is crucial for several reasons:

  • Reduced App Size: 
  • Images can significantly contribute to the overall size of your app. 
  • Smaller app size is especially important for users with limited storage or slower internet connections.
  •  By optimizing images, you can reduce the app’s size, leading to faster download and installation times for users. 
  • Faster Loading Times:
  • Optimized images load faster, which enhances the app’s overall performance and responsiveness, hence retaining user engagement and reducing bounce rates.
  • Support for Different Screen Sizes:
  • React Native apps need to support various screen sizes and resolutions.
  • By providing properly optimized images, you ensure that the app looks good and performs well on different devices.

Follow these best practices to optimize images in your React Native app: 

  • Use image assets with appropriate resolutions for different screen densities (e.g., @1x, @2x, @3x) to avoid unnecessary upscaling or downscaling. 
  • Compress image files without sacrificing quality. Tools like ImageMagick, and TinyPNG can help you achieve this.
  • Use the `resizeMode` prop on the <Image> component to control how images should be resized and displayed.
  • Optimize Expensive Operations

Be mindful of any expensive operations, such as heavy computations or API calls, that could lead to re-renders. 

  • Memoization:
  • Use memoization or caching where appropriate to avoid redundant work.
  • Memoization is a technique that stores the results of expensive function calls and returns the cached result when the same inputs occur again.
  • Debouncing:
  • Debouncing delays the execution until a certain period of inactivity occurs.
  • If you have operations that are triggered frequently (e.g., search input, scroll events), consider using debouncing to limit the number of times the API is being called. 
  • This can be achieved using libraries like `lodash` and `use-debounce` in JavaScript.
  • Lazy Loading:
  • Lazy loading can significantly reduce the initial load time of your app and improve performance.
  • It is used to load heavy content on a page slowly after important content is loaded.
  • Large components and content will be loaded when needed, which helps reduce the loading time.
  • List rendering components like FlatList or Pagination can be used to achieve this.
  • Use React Native’s Built-In Optimization

React Native provides various built-in optimizations, such as `useWindowDimensions`, `shouldRasterizeIOS`, etc. Familiarize yourself with these options and use them where applicable.

  • shouldRasterizeIOS:
  • In React Native, shouldRasterizeIOS is a prop that can be used on certain components to improve performance, specifically on iOS devices. 
  • This prop is available on the View component in React Native.
  • When you set shouldRasterizeIOS to `true`, the component’s content will be rendered to an offscreen bitmap and cached.

Here’s how you can use shouldRasterizeIOS:

import React from ‘react’;
import { View, Text } from ‘react-native’;

const MyComponent = () => {
  return (
    <View shouldRasterizeIOS={true} renderToHardwareTextureAndroid={true}>
      {/* Your component’s content here */}
      <Text>Some complex content here</Text>
    </View>
  );
};

export default MyComponent;
  • useWindowDimensions:
  • useWindowDimensions is a React hook that provides a more convenient and reactive way to access the screen dimensions in functional components. 
  • It automatically updates the dimensions whenever the screen size changes (e.g., orientation changes), ensuring that your components respond to these changes dynamically. 
  • It returns an object with `width` and `height` properties, representing the current dimensions of the window or screen.

Example – 

import React from ‘react’;
import { View, Text, useWindowDimensions } from ‘react-native’;

const MyComponent = () => {
  const { width, height } = useWindowDimensions();

  return (
    <View>
      <Text>Screen Width: {width}</Text>
      <Text>Screen Height: {height}</Text>
    </View>
  );
};

export default MyComponent;
  • Code Splitting

Consider code splitting or lazy loading for large components or screens that are not immediately needed when the app starts.

This can significantly reduce the initial bundle size and improve the startup performance of your app. 

Code splitting is especially beneficial for larger applications that have a lot of code or dependencies.

  • Setup Babel Plugin: 

First, ensure that your React Native project is set up to support dynamic imports. 

To do this, you need to install the Babel plugin called `@babel/plugin-syntax-dynamic-import`.

npm install –save-dev @babel/plugin-syntax-dynamic-import

 

yarn add –dev @babel/plugin-syntax-dynamic-import

Next, you need to add the plugin to your Babel configuration (usually found in the .babelrc file):

 

{
“plugins”: [“@babel/plugin-syntax-dynamic-import”]
}
  • Using Dynamic Imports: 
  • With the Babel plugin set up, you can now use dynamic imports to load components or modules asynchronously. 

For example, let’s say you have a component called `LazyComponent` that you want to split into a separate chunk:

import React, { Suspense } from ‘react’;

const LazyComponent = React.lazy(() => import(‘./LazyComponent’));

const MyScreen = () => {
  return (
    <Suspense fallback={<ActivityIndicator />}>
      <LazyComponent />
    </Suspense>
  );
};

export default MyScreen;

In the above example, the React.lazy function takes a function that returns a dynamic import statement. 

When LazyComponent is rendered, it will asynchronously load the code for the component, and while it’s loading, the Suspense component will render the `ActivityIndicator` or any fallback UI you specify.

  • Handling Errors: 
  • When using React.lazy, it’s essential to handle errors that may occur while loading the dynamic module. 
  • You can use the `ErrorBoundary` component from React to catch and handle these errors.
import React, { Suspense } from ‘react’;

const LazyComponent = React.lazy(() => import(‘./LazyComponent’));

const ErrorFallback = () => {
return <Text>Failed to load component.</Text>;
};

const MyScreen = () => {
return (
<Suspense fallback={<ActivityIndicator />} onError={<ErrorFallback />}>
<LazyComponent />
</Suspense>
);
};

export default MyScreen;

By focusing on these aspects and continuously testing and optimizing your React Native app, you can deliver a high-performance and enjoyable user experience.

Remember to profile your app regularly to monitor performance improvements and identify any new bottlenecks that may arise as your app grows.

2+
Prachi Jamdade

Prachi Jamdade

Prachi is an Android Developer @Business Score and a Software Engineer Intern @Codemonk. She loves to write about her learnings in tech and software development. You can find her on GitHub @Prachi-Jamdade and on Twitter @prachiijamdade

Add comment