How I made my BookDesk mobile app on React Native 3x faster?
Hello everyone! In this article I will share with you my practical experience in optimizing my mobile application on React Native. I'll tell you how I accelerated my application by 3 times.
First of all, I wrote this material for myself to organize my knowledge in the form of a guide to optimizing RN applications. Perhaps this material will be useful to someone.
I am developing my own mobile application BookDesk for storing read books. Previous series: 1, 2, 3
I'm making an Android app, but these tips will also be useful for the iOS version, since I'm developing on React Native.
React Native based on the library react for building user interfaces and everything works identically in both the web version and mobile applications.
So, let's begin!
1. Optimization of interface redrawing (re-rendering)
So, redrawing or re-rendering of components is the main problem of many React Native applications.
First, let's figure out what these redraws are and why they are a problem.
React Native apps have the most “heavy” operations are redrawing elements/components of the interface. Why? Each application has some object model or hierarchical structure of user elements (in other words, what we see on the screen). For example, in web applications[ это DOM (document object model) и она представляет собой глубоковложенную модель элементов со всеми свойствами, атрибутами, тегами и т.д.
Все зависит от сложности интерфейса и количества элементов. Схожая модель и в мобильных приложениях.
Так вот, чтобы отобразить любое изменение на экране пользователя, необходимо вначале найти нужный элемент, внести в него изменения и вызвать перерисовку интерфейса для отображения обновленного элемента. React умеет работать с этим и оптимизирует такие манипуляции путем создания вирутальной модели и с помощью сравнений определяет сам что было изменено и перерисовывает те элементы которые изменелись с наименьшими потерями в производительности.
В приложениях с малым количеством элементов или простым интерфейсом этого может быть достаточно и, возможно, не надо проводить дополнительных работ по оптимизации (но я советую применять оптимизацию не зависимо от размера или сложности приложения). Любое приложение имеет свойство разрастаться и увеличиваться в масштабе и здесь без оптимизации будет тяжело получить качественный продукт. В какой-то момент времени придется столкнуться с проблемами в виде тормозов и фризов. Такие элементы как списки, сетки (grids) для отображения данных, при этом списки могут содержать большое количество динамических элементов с картинками, формами, контролами, кнопками и т.д. все это создает определенную нагрузку на интерфейс и на саму модель. В этом случае без оптимизации можно получить множество проблем в виде задержек, лагов при работе с интерфейсом и виной тому все те же перерисовки. А причина этому кроется в неправильной структуре приложения, поэтому грамотная структура это уже полдела.
Почему срабатывают перерисовки?
Давайте теперь разберемся почему срабатывают перерисовки. Здесь все очень просто.
Вот основные причины перерисовки компонентов:
Все эти изменения повлияют на переривоку как самого компонента так и всех вложенных в него компонентов. Это очень важно.
Вы должны следить за всеми перерисовками и предотвращать нежелательные. Для этого, вы должны управлять структурой компонентов и разбивать компоненты на более мелкие со своими внутренникми состояними.
const Book = () => {
const [description, setDescription] = useState(''); const [title, setTitle] = useState(''); const [rating, setRating] = useState(''); const [id, setId] = useState(''); return {title} {rating} {description} }
We have a Book component that contains local state. description, title, rating And id and there is also a nested heavy component HeavyWeightComponent. When any of the states changes, the component will be redrawn. Book and nested components and further down the chain, if nested components have nested ones, they will also be redrawn. In other words, if we click on the button that changes the rating, the component will be redrawn Book and component HeavyWeightComponentalthough this component only accepts as props id and we don't want it to be redrawn. How can we prevent it from being redrawn?
Solution
There are several solutions to this problem. I prefer to look for solutions without using additional libraries, hooks, utilities, etc.
1. Use HOC (Higher order component) memo from the react library. It allows you to avoid redrawing the component if the props have not changed. For this component HeavyWeightComponent needs to be redone:
import { memo } from 'react';
const MemoizedHeavyWeightComponent = memo(HeavyWeightComponent)
const Book = () => {
const [description, setDescription] = useState('');
const [title, setTitle] = useState('');
const [rating, setRating] = useState('');
const [id, setId] = useState('');
return <View>
<Text>{title}</Text>
<Text>{rating}</Text>
<Text>{description}</Text>
<Button onPress={() -> setDescription('some description')} />
<Button onPress={() -> setTitle('some title')} />
<Button onPress={() -> setRating('some rating')} />
<Button onPress={() -> setId('some id')} />
<MemoizedHeavyWeightComponent bookId={id} />
</View>
}
Need to import memo from the library react and then create a memoized copy of the component and use it. In this case, we will get rid of unnecessary redrawings and get the expected behavior of our heavy component. HeavyWeightComponent will be redrawn only when the property changes bookId where do we throw it id from the local state of the Book component. This is a good, clear and understandable solution, but if you slightly rework the structure of the component Bookwe can get rid of the use of memo.
const BookHeader = ({ id, title, rating }) => {
const [description, setDescription] = useState('');
const [title, setTitle] = useState('');
const [rating, setRating] = useState('');
return <View>
<Text>{title}</Text>
<Text>{rating}</Text>
<Text>{description}</Text>
<Button onPress={() -> setDescription('some description')} />
<Button onPress={() -> setTitle('some title')} />
<Button onPress={() -> setRating('some rating')} />
</View>
}
const Book = () => {
const [id, setId] = useState('');
return <View>
<BookHeader />
<Button onPress={() -> setId('some id')} />
<HeavyWeightComponent bookId={id} />
</View>
}
We can break down our component Book into several smaller components that will be responsible for their areas. In this case, we can create a component BookHeader and move all local states there except id.
Now we have 2 adjacent components BookHeader and our heavy component. When any of the local states changes in BookHeader redrawing will only occur within the BookHeader. And our heavy component will not be redrawn and will not depend on BookHeader.
Conclusion
Always keep an eye on the structure of your application. It is tempting to bloat an existing component by adding new functionality, but before doing this, you need to think about the structure.
Stick to the rule: one function – one component
2. Caching and memoization
IN react There are several hooks for adding caching. We got a little familiar with this concept earlier using the example of using HOC memo for caching components.
useMemo
Hook for caching calculation results, data arrays. If your components use some heavy calculations, array iteration and changes using methods map, filter, reduce – then it is absolutely necessary to use useMemo. This way, when a component is re-rendered, react will take the cached value of the calculation result and return it instantly (if the dependent values have not changed) instead of constantly recalculating on each component re-render. Example of use
All the above-described entities that I use in components are always passed through useMemo.
useCallback
Hook for caching functions. Every time a component is re-rendered, react re-creates all entities that are inside the component. This includes variables, objects, functions, arrays, elements etc. In order to avoid recreating functions we should use useCallback. I recommend using it for all functions that you create inside components. Example of use
I pass almost all component functions through useCallbackespecially in cases where these functions are used in hooks useEffect or passed on as props to child components.
memo
HOC for caching components. Helps to avoid unnecessary redraws if the component prop values have not changed. As a second argument, it can accept a condition for redrawing the component. For example, if an object with some nesting comes as a prop, we can set a condition that when certain properties of this object change, the component is redrawed, and in all other cases not. Very useful, especially for use in grids and lists. I actively use memo in most components that have props.
Important point! If we pass an object or array, then the comparison will be done by references, which means that the component will always be redrawn even if the values in the array or object have not changed. Therefore, you should use the second argument with the definition of the properties that need to be compared.
In my application I use all these approaches. Knowing these approaches, you can easily start designing optimized components.
In the 19th version of react they promise that by default these approaches will work automatically out of the box and developers will not have to worry about it. But, so far this is not there and when it will appear is unknown, so I advise you to use these approaches now.
These 3 entities of caching and memoization will help to significantly increase the performance of your application and get rid of unnecessary redrawings. Mobile applications, unlike web or desktop applications, are more sensitive to redrawings because there are a large number of weak mobile devices, which means we cannot afford to use unnecessary redrawings and consume device resources. In my case, after implementing the above methods, I increased the response of some functions several times. If earlier the opening of the menu with the book status was ~3 seconds (in the worst case scenario, when a large number of books were loaded and redrawing of all elements on the boards was launched), now this time is less than ~0.5 seconds, a difference of ~6 times.
3. Grid/List Optimization
Today it is difficult to imagine a mobile application that does not use grids, lists in one form or another. My application BookDesk is built and based on grids and lists. We have several boards that we can work with at the same time, and all these boards are grids. This means that we always have a certain number of books loaded on each of these boards. Optimizing grids should be given special attention.
To work with grids, React Native provides components FlatList And SectionList. They work on the basis VirtualizedList and allow you to load entities as needed, have good optimization and functionality. However, they need to be further optimized. React Native gives recommendations on optimizing your grids. I will not dwell on each point from this article in detail, since this topic is worth a separate material.
I will only say the most important thing – it is absolutely necessary to use caching in the form memo For item component and useCallback for all functions of the type renderItem, key extractor etc. to avoid unnecessary redrawings. And of course, use “key extractor” for optimization, this property adds a unique key for each element and when changing/removing elements from the grid, React Native can easily determine which element has changed and quickly redraw everything.
Using the map function
React web applications use JSX to be able to use html inside JS and it is usual to use the function map to render lists or something from arrays. But in React Native this should be avoided, because all this stuff needs to be wrapped additionally in a component ScrollView to create a scroll. This approach has a number of disadvantages and React Native itself recommends using FlatList instead of map + ScrollView..
In my application I use all types of lists including FlatList and SectionList. I prefer to use them for small lists where there is no need to use virtual loading on scroll.
FlashList
For the main large virtual lists with books I use the component FlashList from Shopify. It uses React Native to build its mobile app. Their app is based on huge lists with images and other elements. They need maximum performance, so they developed their own component for lists. Shopify managed to achieve better performance compared to FlatList.
I have personally verified this. Rendering, redrawing and scrolling of lists works flawlessly and lightning fast, without freezes and empty areas, everything works instantly. Shopify also has optimization recommendations. Unlike FlatList in FlashList there is a property “estimatedItemSize” is mandatory to use and is the basis for optimization of the entire list. This is the horizontal or vertical value of the list element (depending on the type of list horizontal or vertical) which can be obtained in the inspector. There is also a property “getItemType“which is necessary to determine the type of element, it can be text, picture, object. I will not list everything, you can read it at the link above, but in FlashList There are fewer such techniques, because out of the box it is already well optimized.
As in FlatList, don't forget to use useCallback for all functions that you pass into the component FlashList and also memo for the list item component.
Tip: Don't overload the list item component with local states, heavy nested components, or complex calculations.
If you need to add local state, it is better to create a small component and define the state in it and add it in turn to the list item component. And all child components should be optimized using memo, useCallback And useMemodon't forget about this important rule.
4. Reselect library for Redux
Just like the hook useMemolibrary Reselect can be used to create memoized selectors to optimize expensive computations. However, unlike useMemothis should be used with Redux.
In my application I use Reselect to obtain processed data from reduxas in the case of useMemowhere array methods are used map, filter, reduce etc. or some complex calculations.
Reselect to cache results from redux.
5. Lazy Loading
Out of the box, react allows you to load components on demand. Until the user goes to a certain screen, react will not load components. This is a great opportunity to reduce the initial load time of the application. For example, why should we load all the screens at once if a new user gets to the page Login? We only load the page Login and all the rest as needed. Likewise, if the user is already logged in and upon startup gets to a page with boards of bookswhy would he need to load screens Login, Registrations, Statistics, Profiles etc. which he is not currently using. This is a powerful optimization tool that I recommend using.
We can import the function lazy from react and wrap all our components in it.
import { lazy, Suspense } from 'react';
const Auth = lazy(() => import('~screens/Auth'));
const BookNote = lazy(() => import('~screens/Home/BookNote'));
const Filtering = lazy(() => import('~screens/Home/Filtering'));
...
<Suspense fallback={<Text>Loading...</Text>}>
<Search />
</Suspense>
...
Use lazy as a wrapper for importing the component and in the place where the component is displayed, wrap it in Suspense. In this way, we create optimization for loading only the components that are needed at a given moment in time.
6. Optimizing images
It is hard to imagine a mobile application without pictures. In my application I use WebP as a format for pictures.
WebP is a modern image format that offers superior lossless compression. Using WebP, webmasters and web developers can create smaller, richer images that speed up the web. Lossless WebP images are 26% smaller in size than PNG. Lossy WebP images are 25-34% smaller than comparable JPEG images at an equivalent SSIM quality index.
On top of that, I use the component FastImg for rendering and caching images. The official recommendation for this component comes from the React Native team.
7. Optimization of production assembly
First, I use the Proguard setting to reduce the build size. Proguard is a tool that can reduce the size of your APK by a small amount. It does this by removing parts of the React Native Java bytecode (and its dependencies) that your app doesn't use. This only works with Android apps. How to set up.
Secondly, I use removing all logs from the code using settings. Console calls also eat up memory and should be eliminated in production builds.
8. Using useNativeDriver in animations
React Native uses 2 threads: JS Thread and UI thread, you can read more about it here HereRunning animations on the JavaScript thread is a bad idea. The JS thread can easily get blocked, which can slow down the animation or prevent it from running at all. Since the Animated API is serializable, you can pass animation details to the native code before the animation starts. This way, the native code will run the animation on the UI thread. This ensures that the animation will run smoothly even if the JavaScript thread is blocked.
Animated.timing(opacity, {
toValue: 1,
duration: 500,
useNativeDriver: true,
}).start();
9. Using Hermes engine
Hermes – is an open-source JavaScript engine optimized for React Native. For many applications, using Hermes will result in improved startup times, reduced memory usage, and a smaller application size compared to JavaScriptCore. Hermes is used by React Native by default and requires no additional configuration to enable.
10. Package versions
Try to periodically review the versions of libraries and dependencies in the file package.jsonwhich you use. Try to use the latest versions of libraries, especially react-native, react because optimization and performance are improved in new versions. Try to find alternatives to using functions from some libraries. At the moment, JavaScript provides wide opportunities for working with arrays, objects, so you can easily find a replacement lodash, ramda libraries in the form of native use of js functions. Before looking for solutions using third-party libraries, always try to find a solution from the React Native box or on JS, because using additional libraries increases the size of your application and the speed of its loading. But if you need to install a package, look at the number of stars, the date of the last release and the number of weekly downloads and this will give an understanding of the popularity of the package and the feasibility of its use.
I adhere to the principle: fewer packages is better!
And one more recommendation, periodically check the packages for size using react-native-bundle-visualizer It will help to analyze all packages and identify the heaviest ones.
11. Debugger
Use debuggers to analyze the performance of the application. Analyze the application redrawings, look at the average frame rate in the UI and JS thread. With this, you can find memory leaks, unnecessary redrawings. In some cases, repeated and unnecessary requests may occur. All this must be analyzed and corrected to achieve good performance.
Summary
As I said, I was able to improve the performance of my application several times using the techniques described in this material.
But the work on optimization does not end there, I am constantly looking for different options and improvements. I develop the application by trial and error. After all, the most valuable experience is the experience gained by oneself. Optimization of the application is a very painstaking and important work. If you apply all the tips from this material, your application will work faster, and in some cases – many times faster.
I would be very grateful if you install my application to store read books BookDesk and leave a review and rating!
Let's make optimized applications!
Thank you all!