Engineering

React Native Render Optimization: Advanced Techniques

avatar

Heriberto V.

Developer

Posted on June 27, 2024

banner

In this blog post, we will look at how React Native render works and the many tools they have to optimize the rendering process.

Due to the platform target, React native has fewer resources available than React Web, which makes performance issues more evident. The goal is to understand when the time for optimization comes and which is the best tool given the situation.

How React Native render works

First, we need to understand how rendering works in React. The code we write is the guideline of the Virtual DOM, an intermediate structure of components that will be passed to the native layer for the platform to render the components. The rendering task is expensive, and the Framework will use this Virtual Dom to decide if a node in the structure requires re-rendering.

  • Our code becomes a representation of the UI.
  • State changes.
  • React attempts to reconcile the changes into the minimal expression before committing to render the UI in the native layer.
  • When everything has been validated, the changes will be sent to the native layer to fully render the new UI; This can be an expensive task.

Although the concept of Virtual Dom is similar to that of React Web and React Native, the implementation is different due to the engine used (Browser vs Native platform). React Native uses the Bridge to share JS events to the native layer, which is a very expensive operation. By using the Virtual DOM as an intermediary, React could avoid unnecessary changes to the real UI and keep the application responsiveness high.

Adding to the expense of rendering a component, whenever a component renders (state or props has changed), so will the children and their own set of children until there are no more in the hierarchy tree. This might look blazingly fast at the start, but the bigger and more complex the app becomes, the more you will see the struggles of the rendering process. This will vary depending on the device you are currently launching the app, but you will definitely get to this point.

img

Optimizing rendering from the start is important because React native runs on a Mobile platform. A flagship Android or iPhone performance won’t compare to a cheap Android phone, performance issues could never show in your test device but happen in many devices in real-life situations. By using the correct optimization tools for each situation, we will preemptively reduce performance issues and minimize potential drawbacks from over-optimization.

We will cover the following optimization tools provided by React:

  • React.memo
  • useMemo
  • useCallback
  • useContext

React.memo

React.memo is a HoC(Higher order component) that implements the concept of Pure function. A component wrapped in it will not re-render unless its props values change.

A parent component can pass to its children the following cases as props:

  • A primitive type variable as prop: string, number, boolean
  • A non-primitive type as a prop: Function, Array, or Object
  • React.memo avoids unnecessary re-renders for primitive props by comparing its previous and incoming props, for the latter, things are not as simple.

For non-primitive variables, the value that React.memo compares is not the variable itself but the address in the memory where it is stored.

img

We can see here that even though the content is the same, the referenced value is not. This is a shallow comparison that React uses to keep calculations fast and simple.

Only non-primitive variables handled by an useState hook are guaranteed to keep the same reference.

The solution to any other case is to implement our own comparison function as a second parameter of React.memo. A good example to compare is:

img

  • Values of relevant primitive props.
  • Relevant fields of an object prop: For example, an object “Person” having name, age and profile_image, phone, and gender. When rendering a contact list, we don’t need to compare age and gender if they are not going to be displayed.

For arrays, it is not worth comparing each item, we usually work arrays handled by state hooks or custom hooks that already save the array reference.

Lastly, for functions there is no straightforward way to compare their values; for that reason, we use other tools along React.memo.

When to use React.memo:

  • The component has a parent that renders often, even though this component does not always depend on those state changes.
  • The component has an expensive rendering tree; this usually means it is in the middle to the top part of a hierarchy tree

When not to use it:

  • The component’s props change often; this could indicate a flaw in composition or state logic.
  • The component is very cheap to render: wrapping every component will introduce memory management issues instead of rendering issues. If a component has no processing attached or is at the end of a hierarchy tree like a text or button, we will benefit more from not wrapping it in React.memo.

useMemo

Similar to React.memo, useMemo prevents changes in the returned value if the input is the same; this time, it is not a component that we memorize. useMemo is used to save the result of a calculation until any of its inputs have changed.

When to apply useMemo:

  • Returning value is used in a Context provider.
  • The result is involved in heavy calculation logic: processing list items like sorting, filtering, or injecting static items into the list. There is no need to use a useEffect and another state hook for those situations.

img

useCallback

img

useCallback is a syntactic sugar used to save a function reference in a useMemo. This effectively retains a function reference ID between renders unless its dependencies change. useCallback utility is very straightforward.

When to use useCallback:

  • We will use the function inside a useEffect (or other kinds of hooks), and the function depends on a state variable.
  • It will be passed down to a child component known to be wrapped in React.memo, this will prevent unnecessary renders of the whole subtree.

When not to apply useCallback:

  • When a function does not depend on any state besides its input, we can safely extract it as a helper function outside the component.
  • When the child receives the function as a prop is very cheap to render. A leaf node in a component tree, like simply rendering a Button, will benefit more from a re-render than keeping the function in memory.
  • Its dependencies change too often.

useContext

Often, we have situations where we handle a state or an interaction function on a parent component and pass it down to a child component for it to call or handle; it could pass down a couple of components down the hierarchy, too. The issue with this situation is that whenever it changes, the whole branch of the hierarchy tree will re-render until it reaches the component that actually uses it. This is called “prop drilling”.

With React’s Context API, we can create a Provider and make the component that actually interacts with the value subscribe to that provider by using useContext.

The themed button could be deep within a hierarchy tree, and only the components that subscribe to the provider will re-render when the theme value changes. If used with care, it is a great way to avoid prop drilling through multiple levels of components.

img

When to use Context:

  • The stored value is used at multiple levels of a component tree, such as the style theme. This can potentially skip rendering intermediate components, reducing the rendering load.
  • The stored value doesn’t change often, for example, the current user profile information or application-wide settings like filters.

Tip: avoid grouping all values in a single context and separate unrelated values like styles from the user profile.

Conclusion

React Native allows us to build dynamic UI regardless of the platform at a near-native level. However, keeping responsiveness becomes increasingly difficult as the complexity of an application grows, highlighting the need for optimization. Premature optimization can be detrimental. Yet, for React Native, where performance disparities across devices can be significant, thoughtful usage of optimization tools from the start becomes crucial to keep application performance high across a wide range of devices.

Share on