React Native Apps’ best 2023 performance optimization tips

Have you wondered how React updates its UI? And more importantly, why optimizing its performance is crucial for improving UX?

React Native UI updates and Optimization

Let’s get the ball rolling! React is the real MVP of today’s development. With its lightning-fast updates and unbeatable UI, it’s no wonder why it’s the go-to choice for building responsive web applications.  React also uses a powerful algorithm called the Virtual DOM to update the UI.

Since React only updates the parts that needs it, we get faster rendering times and snappier UI interactions. Moreover, with React Native’s declarative approach to building UI, developers focus on the what, making building complex user interfaces with ease.

But what happens when your app becomes a humongous and crooked product?

That’s where performance optimization comes in: by minimizing unnecessary re-renders and enhancing component lifecycles, you can keep your app running smoothly, even when handling large data or complex UI. 

Hence, getting a faster, more responsive UX, which is essential for keeping users hooked.

So, whether you’re a seasoned React developer or just starting out, keep yourself up-to-date with the importance of optimizing your app’s performance. The happier the users, the merrier!

JIC You Need a Quick Refresh 

Bear in mind that its functioning with its new architecture! Check our previous post.

Re-rendering in React is the process of updating the Virtual DOM and checking for differences between the old and new UI. If any, React Native updates the native views to reflect the new UI.

Re-rendering can be an expensive process, especially if you have a complex UI with a lot of components. So, to optimize performance, React uses a diffing algorithm to minimize the number of updates needed to the native views.

To conclude, re-rendering in React is a core mechanism that allows developers to avoid nitty-gritty details and manage and update the UI of their mobile applications.

Here’s an example of a React App component with a child component that explains how re-rendering works:

import React, { useState } from "react";
function App() {
  const [count, setCount] = useState(0);
  const handleIncrement = () => {
    setCount(count + 1);
  };
  return (
    <div>
      <h1>Counter: {count}</h1>
      <ChildComponent onIncrement={handleIncrement} />
    </div>
  );
}

function ChildComponent({ onIncrement }) {
  console.log("Child component rendered");
  return (
    <button onClick={onIncrement}>
      Click me to increment the counter in the parent component
    </button>
  );
}
  • The App component has a state variable called count which is initialized to 0 using the useState hook.
  • The App component also has a function called handleIncrement which is used to update the value of count when the child component is clicked.
  • The App component renders a h1 element that displays the value of count, and a ChildComponent component.
  • The ChildComponent component is passed a prop called onIncrement which is set to the handleIncrement function defined in the App component.
  • The ChildComponent component is a simple button that when clicked, calls the onIncrement function passed down as a prop.
  • When the button is clicked, the value of count in the App component is updated using the setCount function.
  • One important thing to note is that the ChildComponent component has a console.log statement that logs a message every time the component is rendered.
  • This can be used to observe when the component is re-rendered.

When you run and click the button, you’ll notice that the ChildComponent component is re-rendered every time the button is clicked, but the App component is not.

This is because when the state is updated in the App component using setCount, React knows to re-render the child components that depend on that state.

We’re going to delve into some techniques and codes, so get ready to try them out. However, here you’ll find some more tips to explore.

Jump on the wagon: Optimization Tips

Build one ABI [for Android]

For android apps, by default you build all the four Application Binary Interfaces (ABIs) : armeabi-v7a, arm64-v8a, x86 & x86_64.

But, you don’t need all of them if you’re building locally and testing on a physical device. This guarantees a 75% reduction of your react native building time.

When using the React CLI, add the --active-arch-only flag to the run-android command. This ensures that the correct ABI is selected from either the running emulator or the plugged in phone. If you get this message: info Detected architectures arm64-v8a on console, it means that the approach is working fine.

$ yarn react-native run-android --active-arch-only

[ ... ]
info Running jetifier to migrate libraries to AndroidX. You can disable it using "--no-jetifier" flag.
Jetifier found 1037 file(s) to forward-jetify. Using 32 workers...
info JS server already running.
info Detected architectures arm64-v8a
info Installing the app...

This relies on the reactNativeArchitectures Gradle property.

So, when building with Gradle from the command line and without the CLI, you can specify the ABI as follows:

$ ./gradlew :app:assembleDebug -PreactNativeArchitectures=x86,x86_64

This is useful when building your Android App on a CI and use a matrix to parallelize the build of the different architectures.

Though, you can also override this value locally, using the gradle.properties file you have in the top-level folder of your project:

# Use this property to specify which architecture you want to build.
# You can also override it from the CLI using
# ./gradlew <task> -PreactNativeArchitectures=x86_64
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64

When releasing a version of your app, remove those flags as you want to build an apk/app bundle that works for all the ABIs and not just for the one you’re using on your workflow.

Memo-izing!

Memoization is used to optimize the performance of React components by caching the results of expensive computations and reusing them.

In React, memoization is implemented using the React.memo higher-order component. When a component is wrapped in React.memo, React Native will automatically check if the props passed to the component have changed since the last render.

If the props have not changed, the component will not be re-rendered and the cached result will be used instead, improving the performance of the app.

Memoizing components is especially useful for optimizing large and complex components; lists or tables that have a lot of dynamic data and frequently change.

You improve the overall performance and responsiveness of your app. Here’s an example in React using React.memo:

import React from 'react';
import { Text } from 'react-native';

const MemoizedComponent = React.memo((props) => {
    return (
        <Text>{props.text}</Text>
     );
});

export default MemoizedComponent;

In this example, the MemoizedComponent will only be re-rendered if the props.text value has changed since the last render. If not, the cached result will be used saving a lot of processing time.

Get more ideas here!

Optimizing Flatlist Configuration

If you’re building a mobile app with React, you’re likely using the FlatList component to render large lists of data.

It uses a virtualized list rendering system to only render the items that are currently visible on the screen, rather than rendering all the items at once. Here are some key factors to consider when configuring FlatList:

  • Data structure: if you have a large dataset with many nested objects, you may want to flatten the data structure to reduce the number of nested objects.
  • Item height: If you know in advance, you can set the ItemSeparatorComponent to improve performance by preventing unnecessary re-renders.
  • Window size: this prop determines how many items are rendered outside of the viewport. Setting this prop to a lower value can improve performance by reducing the number of items that are rendered at once.
  • Scroll performance: If you’re rendering complex items, use shouldComponentUpdate or React.memo to optimize the rendering performance of each item.

Taking this into account, let’s look now at some examples:

Example 1: Flattening Data Structure

Let’s say you have a large dataset with many nested objects, like this:

const data = [
   {
       id: 1,
       name: 'John Doe',
       address: {
           street: '123 Main St',
           city: 'Anytown',
           state: 'CA',
           zip: '12345'
       }
   },
   ...
]

To flatten this data structure, you can use the map function to create a new array with a flat structure, like this:

const flatData = data.map(item => ({
   id: item.id,
   name: item.name,
   street: item.address.street,
   city: item.address.city,
   state: item.address.state,
   zip: item.address.zip
}))

Then, you can pass flatData to FlatList:

<FlatList
   data={flatData}
    ...
/>

By flattening the data structure, you reduce the number of nested objects and improve FlatList performance.

Example 2: Setting Item Height

If you know the height of each item in advance, you can set the ItemSeparatorComponent to prevent unnecessary re-renders:

const ITEM_HEIGHT = 50;

function renderItem({ item }) {
     return (
        <View style={{ height: ITEM_HEIGHT }}>
              <Text>{item.name}</Text>
        </View>
      );
}

function ItemSeparatorComponent() {
     return <View style={{ height: 1, backgroundColor: 'gray' }} />;
}

function MyFlatList() {
    return <FlatList
    data={data}
    renderItem={renderItem}
    keyExtractor={(item) => item.id.toString()}
    ItemSeparatorComponent={ItemSeparatorComponent}
  />
 );
}
  • In this example, we set the ITEM_HEIGHT constant to 50, assuming that each item in the list has a fixed height of 50.
  • Then, in the renderItem function, we set the height of each item’s View container to ITEM_HEIGHT. Allowing us to avoid unnecessary re-renders of the items, since their height remains constant.
  • Finally, we create an ItemSeparatorComponent function that returns a View with a height of 1 and a gray background color.
  • We pass this function to the ItemSeparatorComponent prop of FlatList to add a separator between each item in the list.

‘useCallback’ and ‘useMemo’ Hooks

useCallback is a Hook that memoizes a function and returns a new function only when its dependencies change.

This can be useful for optimizing performance when a component re-renders frequently but its callback functions do not need to be recreated on every render.

Here’s an example of using useCallback in a React component:

import React, { useState, useCallback } from 'react';
import { Button, Text, View } from 'react-native';

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

   // Define a callback function using useCallback
   const handleButtonClick = useCallback(() => {
     setCount(count + 1);
   }, [count]);

   return (
     <View>
       <Text>You clicked the button {count} times</Text>
       <Button title="Click me" onPress={handleButtonClick} />
    </View>
  );
}

In this example, we define a handleButtonClick function using useCallback. The function updates the state of the count variable using setCount.

We pass the count variable as a dependency to useCallback so that a new function is only created when count changes.

useMemo

useMemo is a Hook that memoizes a value and returns a new value only when its dependencies change.

This can be useful for optimizing performance when a component re-renders frequently but its computed values do not need to be recomputed on every render.

Here’s an example of using useMemo in a React component:

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

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

   // Compute a value using useMemo
   const computedValue = useMemo(() => {
      let result = 0;
      for (let i = 0; i < count; i++) {
         result += i;
      }
      return result;
  }, [count]);

   return (
       <View>
           <Text>The computed value is {computedValue}</Text>
           <Text>You clicked the button {count} times</Text>
          <Button title="Click me" onPress={() => setCount(count + 1)} />
      </View>
   );
}

In this example, we define a computedValue variable using useMemo. The variable is computed using a loop that adds up the numbers from 0 to count.

We pass count as a dependency to useMemo so that computedValue is only recomputed when count changes.

Code-splitting in React Native

“When a React application renders in a browser, a bundle file containing the entire application code loads and serves to users at once. This file generates by merging all the code files needed to make a web application work.” Check this blog!

The idea of bundling helps due to the fact that it plummets the number of HTTP requests a page can handle. You’ll reach the point in which this continuous file-soar slows the initial page load making users feel reluctant to use it.

With code-splitting, React allows us to split a large bundle file into several chunks using dynamic import() followed by using the React.lazy.

To implement code-splitting, we transform a normal React Native import like this:

import Home from "./components/Home"; 
import About from "./components/About";

Into something like this:

const Home = React.lazy(() => import("./components/Home")); 
const About = React.lazy(() => import("./components/About"));

This syntax tells React to load each component dynamically. So, when a user follows a link to the homepage, for instance, React only downloads the file for the requested page.

After the import, we must render the lazy components inside a Suspense component, like so:

<React.Suspense fallback={<p>Loading page...</p>}> 
    <Route path="/" exact> 
       <Home /> 
    </Route> 
    <Route path="/about"> 
       <About /> 
     </Route> 
</React.Suspense>

The Suspense allows us to display a loading text or indicator as a fallback while React waits to render the lazy component in the UI. Try it:

 

import React from "react";
import { BrowserRouter as Router, Route } from "react-router-dom";


const Home = React.lazy(() => import("./components/Home"));
const About = React.lazy(() => import("./components/About"));


function App() {
     return (
        <Router>
          <React.Suspense fallback={<p>Loading page...</p>}>
            <Route path="/" exact>
              <Home />
            </Route>
            <Route path="/about">
              <About />
            </Route>
       </React.Suspense>
   </Router>
);
}

export default App;

Find a Useful Video to Dig in!

Time to call it a day!

All in all, optimizing React performance is essential to creating a fast, responsive, and user-friendly mobile app. Here you’ll find more tips.

By following best practices such as reducing component rendering, minimizing the use of third-party libraries, and utilizing tools like Performance Monitor.

Testing and profiling the app regularly can help identify and address any performance bottlenecks. By prioritizing performance optimization, developers can create high-quality React Native apps that provide a seamless user experience. 

Go browse our blog and Instagram for improving optimizationSplash Screens in Reactanimationsvision cameravideo, image pickervector icons and maps!

Keep on reading our latest for more and comment, remember: Sharing is Caring!