19
Improving React Native app performance
I'm not a great writer, and I'm certainly not great with putting my thoughts into words; this is my first post, so I'm going to keep it sweet and short.
React Native is a great framework for rapidly building mobile (actually, cross-platform) apps, but it has a reputation of being slow due to its JavaScript nature.
Let's be honest: a well-written React Native app can be indistinguishable from a well-written native app.
A lot of people expect performance issues to be rooted in React and native views integration, but in a majority of cases problems are actually only on the React side.
I'm working on an app which contains a few dozen of views in it, and one reoccurring performance bottleneck in our JS thread has always been related to Redux store updates.
This isn't a very well noticeable issue on the web: your user switches the page and its components will be gone with it too.
On mobile, however, your app has to maintain a view hierarchy. When a new screen is pushed onto the nav stack, your previous screens with its components will be kept alive too.
These components are hidden down the stack hierarchy and are not visible to the end-user, but will still take up extra resources and be updated/re-rendered whenever Redux state that your component has been subscribed to changes.
react-navigation
provides a hook called useIsFocused
, which allows your component to render different content based on the current focus state of the screen.
By using it, we can create our own useSelector
hook for Redux, which will only return fresh selected state when our screen is in focus:
import { useRef } from 'react';
import { useIsFocused } from '@react-navigation/core';
import { useSelector } from 'react-redux';
const undefinedRef = Symbol();
export function useAppSelector<Selected = unknown>(
selector: (state: RootState) => Selected,
ignoreUnfocusedScreens: boolean = true,
equalityFn?: (left: Selected, right: Selected) => boolean,
) {
const memoizedSelectorResult = useRef<Selected | Symbol>(undefinedRef);
const isScreenFocused = useIsFocused();
return useSelector((state: RootState) => {
if (
memoizedSelectorResult.current === undefinedRef ||
!ignoreUnfocusedScreens ||
isScreenFocused
) {
memoizedSelectorResult.current = selector(state);
}
return memoizedSelectorResult.current as Selected;
}, equalityFn);
}
That's it! 🎉
There's no math, no statistics, I'm not going to surprise you all by making false claims like "woah get yourself a 500% performance improvement simply by adopting these 20 LoC in your project", but after implementing it myself I've noticed large improvements in JS thread performance due to cutting off unnecessary re-renders of "heavy" and inactive screens.
Honestly, I'm very surprised that this problem isn't talked about as often as it should be. At least I didn't find myself any article about this particular case. I tried.
I don't think that my solution should be the way to go forward when working with Redux in a complex mobile app, but fortunately the folks over at Software Mansion are actually doing something even better to tackle this problem.
Thank you for your attention.
19