Have you wondered how React updates its UI? And more importantly, why optimizing its performance is crucial for improving UX?
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!
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 calledcount
which is initialized to 0 using theuseState
hook. - The
App
component also has a function calledhandleIncrement
which is used to update the value ofcount
when the child component is clicked. - The
App
component renders ah1
element that displays the value ofcount
, and aChildComponent
component. - The
ChildComponent
component is passed a prop calledonIncrement
which is set to thehandleIncrement
function defined in theApp
component. - The
ChildComponent
component is a simple button that when clicked, calls theonIncrement
function passed down as a prop. - When the button is clicked, the value of
count
in theApp
component is updated using thesetCount
function. - One important thing to note is that the
ChildComponent
component has aconsole.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.
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
orReact.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’sView
container toITEM_HEIGHT
. Allowing us to avoid unnecessary re-renders of the items, since their height remains constant. - Finally, we create an
ItemSeparatorComponent
function that returns aView
with a height of 1 and a gray background color. - We pass this function to the
ItemSeparatorComponent
prop ofFlatList
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 optimization: Splash Screens in React, animations, vision camera, video, image picker, vector icons and maps!
Keep on reading our latest for more and comment, remember: Sharing is Caring!