Nobody wants to use a slow or unresponsive app. Organic growth of an app is crucial to the success of a business. If the first impression of an app is not good, most users will not return. User retention rate will also falter and affect the growth of the business. Low ratings and poor reviews can further affect the search ranking on both the Play Store and App Store. It is crucial for tech teams to know how to build lag free mobile apps.
Improving the performance of an app is a complex task. Expecting it to be fixed in one shot may not be the most reasonable expectation. Given these factors, launching a poorly developed app becomes an even less advisable choice.
React Native can deliver near-native apps for Android and iOS apps. It provides a way to write code without focusing on the performance. But we can’t ignore the best practices. If you want your app to be loved by users, you should ensure high code quality from Day 1.
There are different ways to ensure React Native app performance optimization and monitor the mobile app for future changes. In this blog, I will show you different ways to track and improve the performance of a React Native app for both Android and iOS.
Top 10 best practices to build lag free mobile apps
While writing code for React Native app performance optimization, you should always follow these 10 practices to maximize the outcomes-
1. Use Hermes:
Hermes is the default engine for React Native version 0.70 and above. If you want to work on React Native android performance and are using version 0.60.4, you can enable Hermes manually for Android. Sound Hermes performance in React Native for iOS needs at least version 0.64.
To know how to enable Hermes in React Native on Android, you need to edit the android/app/gradle.properties file:
hermesEnabled=true
Add the following lines in your proguard-rules.pro file:
-keep class com.facebook.hermes.unicode.** { *; }
-keep class com.facebook.jni.** { *; }
That’s all! You need to clean and rebuild your app to make it work:
cd android && ./gradlew clean && cd .. && npx react-native run-android
For iOS, you need to update the hermes_enabled property in your ios/Podfile:
:hermes_enabled => true
Make sure to install Hermes pods before you restart the App:
cd ios && pod install && cd .. && npx react-native run-ios
If you want to move back to JavaScriptCore engine, you can flip these flags to false.
2. Image handling:
a. Image formats:
Knowing how to adjust large images in React Native is crucial as image formats can influence react native app performance. PNG is preferred over JPEG or JPG on Mobile applications. WebP is another image format introduced by Google in 2010. It can provide an average 30% or more compression than JPEG without losing the quality of the image. For WebP support, you need at least Android 4.2.1 and iOS 14.
WebP is also natively supported in most of the web browsers including Google Chrome, Safari, Firefox, Edge, and Opera browser.
b. Image Library:
React Native provides an Image component to work with images. This component’s performance is inefficient for remote images, high-resolution images, and bigger images. High-resolution and big images can lead to memory leaks. Even the app can crash if you don’t handle them properly. This can be improved with smaller images, different images for different-sized image components, or by caching the images to the disk.
We can use a third-party library to handle many of the above and improve react native app performance. The library react-native-fast-image is a popular package available for both Android and iOS. It is a wrapper for Glide (Android) and SDWebImage (iOS). It has many popular features like caching, adding auth headers, preloading images, GIF support, adding border-radius, etc.
c. Image Caching:
The Image component takes a cache property to handle how network requests interact with the local cache. We can pass force-cache to this prop to load the image from the cache if it is available.
<Image
source={{
uri: 'https://site.com/logo.png',
cache: 'force-cache',
}}
style={{width: 200, height: 200}}
/>
The cache control of Image is available only on iOS.
We can use react-native-fast-image to use caching on both Android and iOS. It has three properties for caching:
i) FastImage.cacheControl.immutable
It is the default cache control. It will update the image only if the URL changes.
ii) FastImage.cacheControl.web
It will cache the images like browsers. It uses headers and normal caching behavior.
iii) FastImage.cacheControl.cacheOnly
It loads the image from the cache without making any network requests.
import FastImage from 'react-native-fast-image'
const YourImage = () => (
<FastImage
style={{ width: 100, height: 100 }}
source={{
uri: 'https://site.com/logo.png',
cache: FastImage.cacheControl.cacheOnly
}}
/>
)
d. CDN:
An Image CDN can speed up the delivery of network images. It can also transform and optimize the images for different sizes as per requirements. The CDN creates a new type of image if it still needs to be created. We can manipulate the image properties on the fly, for example, cropping the image, converting it to a different format, changing the size per the device width, etc.
3. Inline functions:
We can pass an inline function as an argument to another function, or we can assign it to a variable created at runtime and pass that variable as an argument to the function. Inline functions are commonly used as callback functions, for example, on clicking a Button or pressing a Pressable.
<Button
onPress={() => {
setClicked(!clicked);
}}
title="Click here"
/>
Similarly, we can also pass an inline function to a child component. For example,
function ChildComponent(props) {
return <Button title="Press me" onPress={props.onPressButton} />;
}
export default function ParentComponent() {
return <ChildComponent onPressButton={() => console.log('button clicked!!')} />;
}
In the above example, an inline function is passed to the onPressButton props. We should avoid this practice because if the ParentComponent re-renders, this function will be created again and the ChildComponent will re-render even though nothing is changed in its props.
4. Re-rendering:
There are different ways available to avoid re-rendering of components in React Native. We should memoize the components to avoid unnecessary re-rendering. We can either use the React.memo HOC or useMemo hook to memoize a component.
Memoization will help in avoiding re-rendering a component if the props of a component are not changed. We can use a separate function for complex props to decide when to re-render a component.
Another hook, useCallback can be used to get a memoized version of a callback. It can be used to avoid running a function on each re-render.
5. Optimize the performance of huge or infinite lists:
React Native provides two components to render a list with scrollable items: ScrollView and FlatList. ScrollView is used for a finite number of items and loads the data simultaneously. It can affect the app’s performance if we use it for a large set of data.
On the other hand, FlatList renders only a small data set and removes them if they are moved out of the screen. By default, it is 10, but this value is customizable. If the data set is huge, FlatList has a significant advantage over ScrollView. It can provide most of the commonly used features of a list, for example, horizontal mode, pull to refresh, support for header/footer and separators, etc.
For lists with sections, SectionList is preferred over any other custom lists. It is optimized to be used with huge data sets.
If you want to explore other third-party libraries, you can check flash-list or recyclerlistview.
6. Remove console logs on production builds:
console.log statements are used for printing debug and error logs. Leaving these statements on production builds can affect the performance of the JavaScript thread. console.log statements are useful for debugging, and we can use the babel-plugin-transform-remove-console dependency to remove the log statements automatically on release builds.
It can be installed with npm or yarn:
npm install babel-plugin-transform-remove-console --save-dev
// or
yarn add babel-plugin-transform-remove-console --dev
Once it is installed, we can add it to the .babelrc file:
{
"env": {
"production": {
"plugins": ["transform-remove-console"]
}
}
}
7. Avoid .gif files:
Animations can improve the user experience of an app. Using .gif files can cause some big bottlenecks in the application screens. Lottie is a library to render Adobe After Effect animation files natively. This library works with iOS, Android, and Windows. Developers can get the .json files from the designers and render complex animations without worrying about the complexity.
8. Use NativeDriver for Animation:
React Native Animated API uses JS thread. It is not a good idea as the JavaScript thread might get blocked and slow down the app. We can move the animation processing to run on the UI thread. This will run the animation in a separate thread and keep the JS thread free.
We need to mark the flag useNativeDriver to true.
Animated.timing(this.state.currentValue, {
toValue: 1,
duration: 500,
useNativeDriver: true,
}).start();
9. Optimize Android app size:
The size of React Native applications is larger than native applications. Big size can be a reason for a low retention rate. There are different ways to keep the application size on track. A few of these are:
– Remove unused third-party libraries or always check for the size difference after a library is added.
– Compress the images and remove unused resources like fonts, images, etc.
– Set enableProguardInReleaseBuilds to true to enable ProGuard for the Android build. Update the ProGuard rules always if required.
– Always upload Android App bundles on PlayStore.
10. Improve memory usage:
Your app may crash on low-memory devices if it has a memory leak or if the memory usage is increasing gradually. We can use Android Studio and XCode to monitor and fix it. We should also monitor network calls as well. Flipper provides a network plugin to test all API requests and responses with data.
Tools to monitor Mobile App performance:
The React Native app performance optimization depends on various factors, including device hardware, OS version, free memory left, etc. While developing an application, we use a limited number of devices. We can use a device farm, but that will again provide only a limited set of devices, and it won’t be possible to ensure the app works flawlessly on all types of phones. You need a constant monitoring of app performance indicators to build lag free mobile apps.
There are different tools to monitor app crashes and performance during development time or after it is published on production. A few of these are:
Flipper:
Flipper is a platform to debug React Native applications. It comes with a couple of useful tools and support for other extensions. Flipper makes the debugging easier. You can use it to debug JavaScript or native logs, network requests/responses, Async Storage, etc. To measure the performance of app screens, react-native-flipper-performance-monitor plugin can be used.
Similar to Flipper, react-native-debugger is another app for debugging react native applications. It provides a couple of useful extensions like react inspector, redux dev tools, Apollo client dev tool, etc.
Google Play Console Android Vitals report:
Android vitals are technical quality reports collected on apps downloaded from Google Play. It provides data on stability metrics, performance metrics, battery usage, and permission denials. You can get data on user-perceived ANR rate, crash rate, excessive wakeups, partial wake locks, excessive background Wi-Fi scans, background network usage, startup time, slow rendering, frozen frame, and permission denials.
These reports are available on the Google Play console, and we can analyze these to improve the performance of the app. Generally, improving the performance on Android phones will also improve on iPhones for React Native.
Third-party performance monitoring tools:
We can get an overview of the performance with Google Play Console or Flipper. But, to find out the exact screen, code, or UI component, we need to use performance monitoring tools on the production release of the app.
1. Sentry: Sentry is an error-tracking and performance-monitoring platform. With React Native applications, we can use it to track different performance metrics like start time, routing instrumentation, touch/gesture events, slow/frozen frame, stall tracking, React components profiling, network traffic, etc.
2. Firebase performance monitoring: For firebase performance monitoring in React Native applications, we need to use the react-native-firebase package. It can be used to track network traffic, startup time, custom traces, and screen rendering.
Conclusion:
Users always ask for a flawless experience. It makes the React Native app performance optimization all the more crucial. Developers can make this happen by keeping an eye on the performance metrics and update the application with best practices .
In this blog, I have discussed how to build lag free mobile apps by improving react native app performance. Requirements vary from product to product. That’s why we, at Talentica Software, a software product development company, groom the user story first to develop effective customized solutions. We have followed this approach and innovated while building 185+ tech products for VC-funded startups and large tech companies.