Why Optimizing Performance in React Native App is Important?
We live a fast life, and people are used to comfort. Short films are becoming increasingly popular. Instead of wasting time on a trip to the store, they prefer to make quick purchases online. This also translates into expectations related to the use of websites or mobile applications.
Even the smallest cut, a slightly longer waiting time for a given subpage to load can frustrate the user, which can ultimately result in the app abandonment. It is therefore worth paying special attention to even the smallest shortcomings of our application and making the loading times as short as possible. Performance is crucial, so each and every day developers face the growing challenge of optimizing app speed.
Cross-platform development which allows us to create an app for both iOS and Android with just one codebase has recently surged in popularity. However, with the benefits comes the responsibility to ensure they perform flawlessly across both platforms and all supported devices. And tools like React Native, while powerful in enabling cross-platform development, require customized strategies to meet the unique requirements of each platform.
In this article, we will discuss optimizing performance in React Native app with various code optimization techniques, tricks and hacks, and tools and libraries.
The Most Common Problems Of a Poorly Optimized Mobile Application
Below I will present the most common problems of mobile applications. If something looks familiar and you have such a problem in your app, well, this article is for you.
- When scrolling through a long list of items, the app starts to stutter, making the scrolling experience unstable and frustrating
- While browsing through the gallery in the app, images take a long time to appear
- There is no indication that an image is loading, leaving the user uncertain if the image will eventually load or if there is an error
- Animations in the app, such as screen transitions or bottom sheet slides, are laggy and lack smoothness, giving the app a “sluggish” feel
- Switching between different sections of the app takes too long, and sometimes screens freeze before fully loading
- When using the app offline or with a poor internet connection, data loads extremely slowly, and the app often becomes unresponsive
- After using the app for some time, it begins to freeze or completely shut down, requiring a restart
- When using the app on both iOS and Android, it is noticeable that the app runs less smoothly on one platform than the other
Common Reasons for React Native App Optimization Problems
Optimizing React Native apps can be challenging due to the unique blend of JavaScript and native code and components. Balancing performance across these different layers is crucial but not always straightforward. Here are some of the most common causes of these performance issues:
Poor State Management
Overly complex or inefficient state management of the app, such as suboptimal use of the Context API lead to excessive component rendering.
Effect: Performance degradation, where unnecessary re-renders additionally burden the CPU and memory.
Poor Resource and Memory Management
Apps that do not manage resources efficiently can lead to memory leaks.
Effect: The app can become increasingly slow or even crash after prolonged use.
Poor Image Optimization
Uncompressed images or improper image cache management can lead to bigger app sizes and long app launch and load times.
Effect: Images load slowly and there’s a big memory consumption, leading to potential crashes.
Excessive data download
Downloading excessive data for a specific component (often unnecessary).
Effect: Increased load times and performance degradation.
Bad animation implementation
By default, animations are run on the main thread. However, a heavy animation load on the main thread can lead to problems.
Effect: Load on the main thread, resulting in dropped animation frames.
Best Practices For Optimizing Performance in React Native App
Below is a list of solutions to common optimization problems found in React Native applications:
- Use FlashList
- Optimize images
- Unregister timers and listeners
- Delete console logs
- Use MMKV for key-value storage
- Use useMemo hook
- Use useCallback hook
- Use Hermes Engine
- Use Reanimated2 for complex animations
Use FlashList
The overall performance issue often occurs when dealing with lists. To address this, it’s recommended to use the FlatList component built into React Native, which renders only those elements that are currently visible on the screen. This procedure saves a lot of memory.
However, there’s a great alternative to FlattList that I highly recommend: @shopify/flash-list. The component works similarly to FlatList, so you can easily convert your existing FlatList code to FlashList with minimal effort.
FlashList uses the concept of view recycling. This means it creates and renders only a limited number of views visible on the screen and reuses them when the user scrolls through the list. It is built on top of RecyclerListView, and inherits all its features and benefits while also adding some additional attributes.
It is worth mentioning a very important prop available in FlashList – estimatedItemSize. This is a numeric value that tells the component the approximate size of the items before they are rendered. FlashList uses this information to decide how many items to show on the screen before they are initially loaded.
Below is a graphic comparing the number of FPS (frames per second) of FlashList with that of the regular FlatList.
Source: Flashlist
Here’s an example of usage:
const renderItem = ({ item }) {
return (
<View>
<Text>{item.title}</Text>
</View>
)
}
return (
<FlashList
data={data}
renderItem={renderItem}
estimatedItemSize={20}
keyExtractor={({ id }) => id}
/>
)
Optimize images
Image optimization is such a broad topic that it is worth dividing it into a few steps worth taking:
Use SVG if possible
SVG images are rendered using vector paths so they tend to use less memory when rendered. This significantly impacts performance. SVGs are also typically smaller files than the more commonly used JPG or PNG.
Cache images
Caching is very useful and solves the problems associated with loading and re-rendering images. It works by downloading an image to local storage in the application’s cache directory and loading it from local storage the next time the image is loaded. Caching with the image component is supported only in iOS.
<Image
source={{
uri: 'https://reactnative.dev/img/tiny_logo.png'
cache: 'only-if-cached'
}}
/>
Use PNG instead of JPG
The PNG format is more friendly for mobile platforms than JPG.
Use WebP format
WebP enables the creation of richer images. WebP image files are on average 26% smaller than PNG files. Please note that this format is supported by devices running Android 4.2.1+ and iOS 14+.
Use react-native-fast-image
A replacement for the built-in React Native Image component. Usage is very similar. React Native Fast Image provides several important features that significantly affect image optimization:
- Aggressively cache images.
- Add authorization headers.
- Prioritize images.
- Preload images.
- GIF support.
- Border radius.
Example of usage react-native-fast-image for optimize image loading:
import FastImage from 'react-native-fast-image'
const ImageComponent = () => (
<FastImage
style={{ width: 150, height: 150 }}
source={{
uri: 'https://reactnative.dev/img/tiny_logo.png',
headers: { Authorization: 'authToken' },
priority: FastImage.priority.normal,
}}
resizeMode={FastImage.resizeMode.contain}
/>
)
#### Unregister timers and listeners
When we register listeners, timers or subscriptions in our application, we must remember to **unregister them when remove the component**. Otherwise, even if we are not in these components, they will still trigger events. This will eventually **lead to performance issues and an increase in unused memory**.
Example of an unregistered timer listener:
import React, { useEffect, useState } from 'react'
import { View, Text } from 'react-native'
const Timer = () => {
const [counter, setCounter] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setCounter(prevCounter => prevCounter + 1)
}, 1000)
return () => clearInterval(timer)
}, [])
return (
<View>
<Text>Counter: {counter}</Text>
</View>
);
};
export default Timer
Delete console logs
Console logs are a developer’s friend when debugging JavaScript code, but they are intended for development purposes only. Console logs can influence the performance of your React native app.
Tip: Use babel-plugin-transform-remove-console plugin
to quickly remove all console logs from the production build.
Use MMKV for key-value storage
MMKV is a high-performance key-value storage third-party library in React Native. It is known for its speed and efficiency and offers a simple interface for storing and retrieving data. Compared to other data storage methods such as AsyncStorage, MMKV provides significantly better performance, especially for large amounts of data.
Key Features of MMKV:
Performance: It is optimized for fast data writing and reading, making it ideal for applications that require high speed.
Multiple Data Type Support: MMKV supports various data types, including strings, integers, and floating point values, as well as arrays and objects.
Encryption: It supports encryption, which means that data can be stored securely.
Cross-platform: It works on both iOS and Android, making it a versatile tool for cross-platform applications.
Large Data Handling: It can handle large amounts of data without significantly affecting performance.
Below is a graph showing the speed of reading a value of MMKV compared to the competition:
I mention MMKV in my latest article, Best React Native Tech Stack – check it out!
Use useMemo hook
Hook useMemo lets you memoize the result of a calculation so that it doesn’t have to be re-executed every time the component is rendered. Using useMemo, we can specify that a given value should only be recalculated if the dependencies we specify in the dependency array change.
How does useMemo affect app optimization?
Reduce unnecessary computations
When we have a function that performs expensive computations e.g. filtering large data sets, useMemo allows us to avoid performing these computations every time the component renders.
Improve rendering performance
In React apps, every render of a component causes all the functions and computations contained in that render to be called. useMemo reduces the number of these operations, which can lead to noticeable performance improvements, especially in large apps.
Example:
const UserList = ({ users }) => {
// useMemo remembers the filter result until 'users' changes
const activeUsers = useMemo(() => {
return users.filter(user => user.isActive);
}, [users]);
return (
<FlatList
data={activeUsers}
keyExtractor={({ id }) => id}
renderItem={({ item }) => <Text>{item.name}</Text>}
/>
);
};
Use useCallback hook
Similar to useMemo, useCallback allows you to “remember” a value (in this case a function) to avoid recreating it each time the component is rendered.
Syntax:
const memoizedCallback = useCallback(
() => {
// Function you want to remember
},
[dependencies], // Dependency
);
Remember!
Hook should be used during React Native Development only when necessary. Otherwise, they can even worsen performance problems.
It is therefore best to use useCallback hook only when we see performance problems.
Use Hermes Engine
Hermes is a JavaScript engine developed by Facebook (now Meta) and optimized for React Native apps.
Key Features of Hermes
- Faster Startup – Hermes optimizes the startup time of your app, which leads to faster startup and shorter app load times.
- Reduced Memory Usage – Thanks to optimized memory management, Hermes uses less memory compared to other JavaScript engines. This can lead to a lower app load and better performance on resource-constrained devices.
- Fast Code Parsing – Hermes uses an optimized byte format for JavaScript code, which speeds up the parsing and compilation time of scripts.
- Debugging and Profiling – Hermes offers debugging and profiling tools that help developers analyze and improve the performance of their apps.
Hermes is currently enabled by default in React Native in your app.
If your version of React Native does not support Hermesyou need to upgrade it to an acceptable version. Check how to upgrade your React Native version.
Use Reanimated2 for complex animations
Reanimated offers advanced animation capabilities that are more efficient compared to native React Native animations. It allows you to create smooth animations that are rendered on a native thread, which reduces the load on the main JavaScript thread.
Performance Monitoring Tools
When building a React Native app, it’s a good idea to familiarize yourself with the Performance Monitoring Tools and monitor the metrics on the go. This will make it easier to find the cause of performance issues.
During the debugging process, Performance Monitoring helps you identify and fix the performance issues we mentioned in the previous chapter, such as excessive re-rendering and memory leaks.
Performance Monitoring is a built-in tool in React Native so you don’t need to install any additional libraries to use it.
To use it all you need is use shortcut:
- iOS Simulator – CMD + D
- Android Emulator – CTRL + M
Then you need to select the Show Perf Monitor option. Below are screenshots of how the window looks like in the case of Android and iOS.
After running the tool you should notice a list of pointers as in the screenshot below.
Performance Monitoring Metrics
Below, we’ll discuss the Performance Monitoring metrics that can help us better understand how the app is working.
- RAM: Displays the amount of memory used by the app. This includes both native and JavaScript memory. Your goal is to keep this number as low as possible for app optimization purposes. The higher the number, the higher the risk of device memory usage and crashes.
- JSC: Displays the amount of memory used by the JavaScript Core engine. This is a subset of RAM usage. Similar to RAM usage, your goal is to keep this number as low as possible.
- Views: Displays the number of UI components created and destroyed by the app. The first number is the current number of views, and the second number is the peak number of views. To avoid unnecessary rendering and memory allocation, aim for the lowest possible numbers.
- UI: Displays the frame rate of the main thread, which handles native UI rendering and user interactions. It is measured in frames per second (FPS). To ensure smooth operation of the application, this number should be around 60.
- JS: This is the number of frames per second of the JavaScript thread that handles business logic such as API calls. Here, too, you should want this number to be around 60.
Once you understand the metrics, monitoring them while adding new features to the app would be easier.
Conclusion
In such a crowded mobile app market and plenty of options available to users, ensuring that your app runs smoothly and efficiently is key to retaining engagement and standing out among competitors.
Cross-platform mobile apps, such as those built with React Native, must perform extremely well on both iOS and Android devices to meet user expectations. A seamless experience boosts user satisfaction but also reduces the likelihood of app abandonment due to slow performance or unresponsiveness.
To achieve this, it’s essential to focus on performance optimization from the outset. This includes managing app resources effectively, minimizing loading times, and ensuring smooth transitions and animations. By addressing these aspects, you can create a robust and responsive app that delights users regardless of their device choice.
By following the tips outlined above, you can develop a React Native app that functions seamlessly on both iOS and Android, offering a consistent and high-quality user experience across all platforms.
Top comments (1)
Love the breakdown of common issues & solutions - like using FlashList for smooth scrolling and MMKV for efficient storage.