Improving your React Native application performance with react-navigation-focus-render

kylessg

Kyle Johnson

Posted on February 20, 2022

Improving your React Native application performance with react-navigation-focus-render

Performance in React Native has always been a bit of a battle, great performance is achievable but is a lot more sensitive to unoptimized code than traditional web development.

Background

I'd recently discovered an issue in my application where my home tab screen had a collection of components that contained multiple re-renders.

The problem was quite straightforward to resolve, but during this, I found out that this non-performant screen was slowing down the other tabs within my application.

A simple example of why this happened

Let's make a simple example that replicates this issue.

ExpensiveComponent

Here's our component that's causing the issue, every time this renders we'll see an obvious performance hit. It's connected to redux and will re-render whenever the state changes for the count.

const ExpensiveComponent = () => {
  const {count} = useSelector((state) => ({
    count: state.count,
  }));
  return  (
    <>
      {!!count && <Text>Count is {count}</Text>}
      {new Array(5000).fill(0).map((v, k) => (
        <Text key={k}>{v}</Text>
      ))}
    </>
  );
};

Enter fullscreen mode Exit fullscreen mode

HomeScreen

Our home screen renders the expensive component and lets us go to screen 2. As long as ExpensiveComponent is re-rendering we will see an obvious performance hit.

const HomeScreen = () => {
    const navigation = useNavigation();
    const goScreen2 = ()=>{
        navigation.navigate('Screen2')
    }
    return (
        <>
            <Button
                title={'Go to Screen 2'}
                onPress={goScreen2}
            />
            <ExpensiveComponent />
        </>
    );
};
Enter fullscreen mode Exit fullscreen mode

Screen2

Our second screen has no performance issues by itself, it contains a button that dispatches an action to update the count. It doesn't render much and you'd expect pressing the button and displaying an updated count to be immediate.

const Screen2: React.FC<ComponentType> = ({}) => {
    const {count} = useSelector((state) => ({
        count: state.count,
    }));
    const dispatch = useDispatch();
    const setCount = useCallback(
        (data: number) => {
            return dispatch(AppActions.setCount(data));
        },
        [dispatch],
    );
    const onPress = ()=> {
        setCount((count || 0) + 1)
    }
  return (
      <Button
        onPress={onPress}
        title={`Update Count (${count || 0})`}/>
  );
};
Enter fullscreen mode Exit fullscreen mode

You would expect Screen2 to have no performance issues right? Wrong. Pressing the update count button was consistently blocking the UI by around 250ms, this can be seen by using a tool I made, react-native-performance-monitor.

React Native Performance Monitor

So why did this happen?

The reason made sense, other tabs were using updating the state that was also used on the HomeTab and as it turned out, inactive tabs will re-render even if they aren't shown.

Even with optimized components, the fact that this happens is useful to be aware of. If you have 5 tabs in your app that have been visited in a session, any state updates can trigger re-renders on all of them.

Preventing this behavior with react-navigation-focus-render

This is where my new npm package comes in. By wrapping the render our ExpensiveComponent in <FocusRender, children are not re-rendered until the screen is focused

const ExpensiveComponent = () => {
  const {count} = useSelector((state) => ({
    count: state.count,
  }));
  return  (
    **<FocusRender>**
      {!!count && <Text>Count is {count}</Text>}
      {new Array(5000).fill(0).map((v, k) => (
        <Text key={k}>{v}</Text>
      ))}
    **</FocusRender>**
  );
};

Enter fullscreen mode Exit fullscreen mode

Simply by adding this, our example is more performant. Here's the comparison to prove it:

Focus Render

This shows an average render time of around 6ms vs the original 250ms.

How does this work?

It's quite simple, This module works by preventing screen re-renders of inactive screens until that screen is focused.

The entire code can be found here. It uses the useIsFocused() hook provided by react-navigation combined with a classic shouldComponentUpdate, returning true only if the screen is focused.

To illustrate this clearly, here's how the above example behaves.

Example

As you can see, it's noticeable when this re-render occurs due to how expensive the component is. However, in cases less extreme, it's more likely to behave like the following:

Example2

Conclusion

Although this library should not replace optimizing components, I believe with big applications this library can introduce big performance benefits.

Let me know if you find this useful, you can check it out on GitHub :)

Happy Hacking!

💖 💪 🙅 🚩
kylessg
Kyle Johnson

Posted on February 20, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related