- Key Takeaways
- Balance Animations between the JS Thread and the Main Thread
- Avoid Unnecessary Re-rendering
- Optimizing Images
- Use Stable NPM Packages
- Use Stylesheet to the Component
- Use Flatlist for Performance
- Avoid Memory Leaking
- React Native Debugging and Performance Monitoring Tools
- Remove Console Logs
- Use babel-plugin-transform-remove-console
- Conclusion
Key Takeaways
- Balancing animations between the JavaScript (JS) thread and the main thread can improve app performance. Utilize the useNativeDriver property and InteractionManager to manage this balance.
- Avoid unnecessary re-rendering of components. Techniques include memoizing components and functions, using useCallback wisely, avoiding updating the local state with Redux state, and avoiding inline functions.
- Optimize images to improve app speed. Use SVG icons and images, WebP image format for lossless image quality, and cache images for faster rendering.
- Use stable npm packages and FlatList for rendering arrays of items, and avoid memory leaks by unregistering timers/listeners/subscriptions, avoiding unnecessary use of global variables, and preventing circular object references.
- Debug and monitor performance using tools such as profiling tools, Chrome’s DevTools, React DevTools, Flipper, and React Native Performance Monitor. Also, remove console logs in production mode to enhance app performance.
Balance Animations between the JS Thread and the Main Thread
In any application, animation is a demanding task, and this holds true for React Native as well. React Native operates with two main threads: the JS thread for executing JavaScript code, and the main thread, which is primarily responsible for rendering the user interface and responding to user input. Animations often run on the main thread by default. However, a heavy animation workload on the main thread can lead to performance issues, such as dropped frames. Let’s consider a scenario where you have a React Native application displaying a draggable element on the screen (such as a draggable ball). The user can drag this element around the screen using touch gestures, and you aim to animate its movement smoothly. In the above scenario, when you drag the ball around the screen, the main thread will be busy gathering user touches. If we execute animations on the main thread, it will burden it further. The result will likely be dropped frames and performance issues. In such scenarios, we can shift the animation task on the JS thread by using the solutions discussed below.Solution 1: Try using useNativeDriver
useNativeDriver
is a React Native animation property that you can use when animating elements in your React Native application.
When the user sets this property’s value to true, the React Native application will render the animation on the main thread. However, if the main thread is expected to be busy with some other task, you can shift the animation to the JS thread by setting useNativeDriver: false
.
Example
Animated.timing(this.state.animatedValue, {
toValue: 1,
duration: 500,
useNativeDriver: false, // <-- Add this to execute animation on the JS thread.
}).start();
In the code above, React Native will check the useNativeDriver
value and it will shift the animation to the JS thread.
You can learn more about animation here.
Solution 2: Use InteractionManager
There will be scenarios where both the JS thread and the main thread will be busy. For example, the application may be fetching API data, performing some logic, and rendering it on the UI. In this situation, the JS thread is occupied with fetching the data and performing logic, while the main thread is busy displaying the UI. When both threads are engaged, attempting to run animations can result in dropped frames and performance issues. In such cases,InteractionManager
can be utilized. You initiate the animation first. Once the animation is completed, React Native will call InteractionManager.runAfterInteractions
to execute JS code. The JS code will then call the API and display data on the UI.
This approach helps avoid overloading the JS thread with simultaneous execution of JS code and animations.
Example
InteractionManager.runAfterInteractions(() => {
/* Code to run after Animation completed */
});
Avoid Unnecessary Re-rendering
Avoiding unnecessary re-renderings in React Native is crucial for maintaining optimal performance. Whenever the app re-renders, the JS thread creates a JS bundle file and passes it through the React Native bridge, which then hands it over to the main thread. The more the application re-renders, the more passes occur between the JS thread and the main thread, which can degrade the application’s performance.Solution 1: Memoize the component
React.memo
is a higher-order component provided by React that is used for optimizing functional components by preventing unnecessary re-renders.
When you wrap a functional component with React.memo
, React will memoize the component, meaning it will only re-render the component if its props have changed. If the props remain the same between renders, React will reuse the previously rendered result, thus avoiding the cost of rendering the component again.
Example
const MyComponent = React.memo((props) => {
// component logic
});
Solution 2: Learn to use useCallback function wisely
When a parent component sets a callback function for its child component, whenever the parent component re-renders, the callback function is also recreated, resulting in a new function reference being returned. Consequently, the child component perceives this as a change in the callback function’s value, prompting it to re-render, even ifReact.memo
is utilized. Therefore, the child component will indeed re-render.
To mitigate this, utilize useCallback
to prevent the recreation of the function reference on every re-render of the parent component.
If you want to use a callback function with new state values, the function has to be recreated. To recreate the function with updated state values, you can utilize the dependency section in useCallback
.
By adding state values to the dependency array, as demonstrated in the example code below, useCallback
will recreate the function only when the state value changes. Consequently, you will obtain a new function reference and updated values as a result.
Example
const memoizedCallback = useCallback(() => {
// callback logic
}, [dependency]);
Solution 3: Try to avoid updating the local state with Redux state
Updating the state with Redux data can result in the component rendering twice: first when the Redux data is updated, and again when the local state is updated with Redux state. By following this approach, we can avoid unnecessary re-rendering of the component. Therefore, try to refrain from updating the local state with Redux-updated store data. Instead, utilize Redux store values directly in the UI. If calculations are necessary before displaying any Redux data initially, then only use the component state with the Redux state.Solution 4: Memoize the function result
Memoize the function result withuseMemo
. This will execute the function and store the value in memory.
When the app calls the function again, it retrieves the data from the memoized storage instead of executing the entire function repeatedly. You can add dependencies to re-execute the function and store a new result.
Example
const handleClick = useMemo(() => {
// handle click
}, [dependency]);
Solution 5: Avoid inline functions
Avoid using inline functions. Instead, use arrow functions withuseCallback
and useMemo
to prevent unnecessary re-rendering.
When a parent component re-renders, functions are also recreated with new references. If we pass any function as a prop to a child component, the child component will also re-render.
To overcome this problem, we can use the useCallback
function to prevent function recreation.
Additionally, by using useMemo
, we can avoid re-rendering the child component.
Also, by using named functions instead of inline functions, your code becomes more readable and maintainable.
Example
<Text onPress={() => pressed()}>Press Me</Text> // Avoid this inline function.
<Text onPress={pressed}>Press Me</Text> // recommended.
The recommended example above will directly call the function instead of creating one more function.
Optimizing Images
Optimizing images can improve app performance. We can utilize the react-native-compressor npm package to reduce the image size for uploading and viewing. Additionally, we can use SVG images to display icons and other images.Solution 1: Use SVG icons and images
SVG files contain XML code describing image/icon paths and lines. SVG images are resolution-independent, allowing them to be scaled to any size without losing clarity. Since SVG images are rendered using vector paths rather than pixel data, they typically consume less memory when rendered in the application. This can lead to improved image load performance, especially on devices with limited memory resources. SVG files tend to have smaller file sizes than raster image formats like PNG or JPEG, especially for simple or geometric graphics. This results in faster download times and reduces the overall memory footprint of the application.Solution 2: Use WebP image for lossless image quality
WebP is a modern image format developed by Google that efficiently reduces image size without compromising quality. WebP images are smaller in size yet maintain good quality, enabling faster display on screens.Solution 3: Cache images to render faster
Utilize caching mechanisms to store previously loaded images, reducing the need to fetch images from the network repeatedly. You can cache URL images using the react-native-fast-image npm package. This library will display cached images immediately on the screen.Use Stable NPM Packages
Opt for smaller-sized and more stable npm packages. These packages not only reduce the size of your React Native application but also ensure efficiency due to their stable codebase. In order to identify the right package for your functionality, consider the following points from the npm website:- Compare different types of npm packages with the same functionality.
- Check the weekly downloads of the npm package. A package with more weekly downloads is preferable.
- Check the npm package size. A package with a smaller size will occupy less space in the project, thus reducing the project size.
- Check the number of releases. A higher number of releases indicates active maintenance by developers.
- Check the last update. This helps determine whether the package is still being maintained by someone.
Use Stylesheet to the Component
Utilizing the StyleSheet module to style components offers the advantage of creating an object only during the initial call. Subsequent calls will reuse the already created style object reference. This approach helps reduce the creation of style objects for every re-rendering.Use Flatlist for Performance
Use FlatList to render arrays of items instead of using ScrollView with the map function. FlatList only renders the items that are currently visible on the screen, which helps optimize the code by rendering only the necessary items.Avoid Memory Leaking
A memory leak is a situation where the program does not release memory (RAM) that it has allocated but is no longer using. Over time, if memory leaks aren’t addressed, they can lead to the program consuming more and more memory, eventually causing it to slow down or even crash due to running out of available memory. We can use profiling tools in Android Studio and iOS Xcode to detect memory leaks. These tools help identify areas where memory consumption is increasing, although pinpointing the exact causes can be challenging. Detecting memory leaks in an app can be difficult, so it’s best to keep the above points in mind to prevent memory leak issues.Solution 1: Unregister timers/listeners/subscriptions
When we register any timers, listeners, or subscriptions, it’s important to unregister them during component unmount. Otherwise, these timers, listeners, or subscriptions will continue to call events even when we are not on those components, leading to increased unused memory.Example
useEffect(() => {
// Registering event listener when component mounts
const handleAppStateChange = (nextAppState: AppStateStatus) => {
console.log("App state changed to:", nextAppState);
};
AppState.addEventListener("change", handleAppStateChange);
// Unregistering event listener when the component unmounts
return () => {
AppState.removeEventListener("change", handleAppStateChange);
};
}, []);
Solution 2: Avoid using global variables unnecessarily
Global variables persist as long as the app is running, because they’re accessible from anywhere in the app’s scope. As a result, the garbage collector interprets them as still in use and doesn’t deallocate them.Solution 3: Circular object references
Creating object references where two objects are pointing to each other will prevent garbage collection, because the garbage collector assumes the objects are still in use.Example
const person1 = {
name: "Alice",
friend: person2,
};
const person2 = {
name: "Bob",
friend: person1,
};
React Native Debugging and Performance Monitoring Tools
Debugging itself is primarily focused on identifying and resolving issues within an application’s codebase rather than directly improving performance. However, through the process of debugging, developers may identify and address performance-related issues such as inefficient algorithms, excessive re-renders, or memory leaks, which can ultimately lead to performance improvements. Debugging and performance monitoring are crucial aspects of developing React Native applications to ensure smooth and efficient user experiences. Below, I will mention some tools for debugging and performance monitoring.1. Profiling tools
Use profiling tools provided by platforms like Android Studio Profiler for Android and Xcode Instruments for iOS. These tools offer insights into CPU usage, memory allocation, network requests, and other performance metrics. You can follow the below steps to open the profiler in both Android Studio and Xcode. Note: identifying memory leaks is not always straightforward, but these tools can help analyze potential issues in the code.Android Studio
Open Android Studio > View > Tool Windows > Profiler. The Profiler window will display CPU and Memory usage. Try accessing different screens in the app and observe the Profiler window to identify which screen utilizes high CPU and memory resources. Then you can check the code and logs to fix any issues.Xcode
Open Xcode > Xcode menu > Developer Tool > Instruments . Once Instruments is selected, a new popup window will appear, offering multiple options such as Activity Monitor, CPU Profiler, Leaks, and so on. If you want to check for memory leaks, click on the Leaks option. Here you’ll find an activity graph along with logs.2. Chrome’s DevTools
Chrome’s DevTools is a commonly used tool for debugging React Native applications. Although primarily designed for debugging web apps, its JavaScript capabilities allow for debugging React Native applications as well. It allows you to inspect network activity, memory usage, and JavaScript performance. To use it on Windows, press CTRL + M, and on macOS, press Command + R. If you’re using a Physical device, then you can shake the mobile device to open the window on your phone. Note: if you’re using React Native 0.73 or later, you can’t use it because it’s deprecated. If you want to learn more about Chrome’s DevTools, check out our browser DevTools secrets article.3. React DevTools
React DevTools allows you to inspect the props and state of individual components in your React Native application. By monitoring changes in props and state, you can identify potential performance bottlenecks. React DevTools provides insights into component re-renders, allowing you to identify components that are re-rendering unnecessarily and optimizing them for better performance. While React DevTools doesn’t offer built-in performance profiling specifically tailored for React Native, you can still use it in conjunction with other profiling tools like Chrome DevTools or Xcode Instruments to monitor performance metrics such as rendering times, CPU usage, and memory allocation. You can run thenpx react-devtools
command to open React DevTools.
You can find out a lot more about React DevTools here.
4. Flipper
Flipper is a powerful debugging tool developed by Facebook that supports React Native. It offers various plugins for inspecting network requests, database queries, and UI rendering performance. The Layout Inspector plugin in Flipper allows you to visualize and inspect the hierarchy of React Native components.5. React Native Performance Monitor
This is a built-in performance monitoring tool provided by React Native itself. It allows you to monitor various performance metrics such as FPS (frames per second), CPU usage, memory usage, and network requests directly within your app. To use it on Windows, press CTRL + M, and on macOS, press Command + R. You will get the show per monitor option. If you want to know more, you can visit the React Native Official website.Remove Console Logs
Console logs can slow down app performance by printing logs to the console. Therefore, it’s advisable to avoid using console logs in production.Solution 1: Use _DEV_ global variable
Use the following code in a utility file and reuse it everywhere. This code will executeconsole.log
only in development mode with the help of _DEV_
, but avoid it in production mode.
Example
const logger ={
log: __DEV__ ? console.log : ()=>{},
error:__DEV__ ? console.error : ()=>{}
}
logger.log("show values" ,data);
In this code snippet, logger.log
and logger.error
will respectively use console.log
and console.error
only if _DEV_
evaluates to true, indicating that the application is running in development mode. Otherwise, they will be set to empty functions, ensuring that they have no effect in production mode.
Use babel-plugin-transform-remove-console
The babel-plugin-transform-remove-console plugin removesconsole.log
in the production build. This results in better performance for your application in production mode.
There are many tools available in the React Native ecosystem for debugging and performance monitoring, including Reactotron, Firebase Performance Monitoring, and more. Explaining all these tools in detail would make the article longer. Therefore, you can focus on using any one of the above tools to identify and address issues.
Conclusion
In conclusion, optimizing React Native app performance is crucial for a seamless user experience. By balancing animations, minimizing re-rendering, optimizing images, and using stable npm packages, developers can enhance app speed and responsiveness. Additionally, leveraging efficient debugging tools and removing unnecessary console logs further improves performance. Prioritizing these optimization techniques ensures peak performance and superior user satisfaction in today’s competitive app market. To check these best practices in action, you can also check out the top React Native boilerplates.Shaik Mukthahar
View AuthorShaik has over five years of experience in React Native development, and specializes in creating mobile applications for both the payment and health domains. Having developed numerous projects from scratch, he's successfully deployed them on the Play Store and App Store. He is passionate about sharing his experience and skills with others, and fostering an environment where everyone can learn and grow together. You can view his portfolio on GitHub.
app performanceReact native
Share this article