Avoiding Wasted Renders in React Native App - Part 1

70% of our learners use our mobile application as their primary mode of learning. This puts a major onus on us to provide the best user experience for our learners that spans a wide range of devices, networks, and tiers. A great user experience is garnered by minimizing frame drops, unresponsive screens, battery drain, and overheating; and all of these issues are directly correlated to heavy CPU usage.

By keeping the CPU usage in check, we minimize the aforementioned issues and deliver a great user experience. Our app is built on React Native we found that one of the main reasons for a high CPU usage in our app was Wasted Renders, which happens when the entire component re-renders when it did not need to.

Before we jump into finding ways to optimize it, we started measuring and profiling the components; to understand the potential impact and scope.

Measure before optimize

why-did-you-render package patches React library to provide logs on why a component was re-rendered. These logs can be directly accessed from the react-native debugger console. The patching slows down React and should not be used in a production environment.

We started profiling the major components and after the first couple of drills, we spot multiple instances where we had wasted renders. After altering all the major Wasted Renders we compiled a list of 10 best practices that anyone could follow to get the best performance of their React application. In this article, we cover the first 5 and the other 5 will be covered in the next one.

1. Use Pure Components for Class-Based Components

One of the easiest ways to avoid wasted renders in most scenarios is by using Pure Components. Pure components have a default implementation for shouldComponentUpdate with a shallow comparison for changes in their props and state.

function shouldComponentUpdate(nextProps, nextState) {
  return (
    shallowCompare(this.props, nextProps) &&
    shallowCompare(this.state, nextState)
  );
}

In shallow comparison, primitive data types like strings, booleans, numbers are compared by value and complex data types like arrays, objects, functions are compared by reference. This means that if you're using a complex data type, you must change its reference whenever there is a change in its value (Immutability). Consider the example given below:

// Initial state in the constructor
this.state = {
  user: { id: "525", name: "nikhil" },
};

// Method 1
let user = this.state.user;
user.name = "abhishek";
this.setState({ user });

// Method 2
let user = Object.assign(this.state.user, { name: "abhishek" });
this.setState({ user });

// Method 3
let user = { ...this.state.user, name: "abhishek" };
this.setState({ user });

// Status is a pure Component
render() {
  return <Status user={this.state.user} />;
}

In the above example, if Method 1 and Method 2 are used for mutating the user object, the state update won't re-render the <Status /> component as the reference of this.state.user remains unchanged. To avoid this, whenever an object mutation has complex data changes, instead of making changes in that object, we can create a copy with changed reference using the spread operator provided by ES6.

Caution: If your component has a large number of props and only a few are changing, using Pure Components will create a lot of computation overhead as it checks for all props in shouldComponentUpdate. In this case, the use of React.Component is advised with a custom shouldComponentUpdate method which performs shallow comparison over the frequently changing props.

2. Use React.memo() for Functional Components

Just like Pure Components, React.memo() provides memorization for props in functional components. All the benefits of PureComponents can be realized using React.memo(). A sample code for the same is given below:

const functionalComp = (props) => {
  // component implementation
};
export default React.memo(functionalComp);

React.memo() does a similar shallow comparison for change in props as we observed for Pure Components. If we need to implement custom shouldComponentUpdate to gain more control over the re-renders of our component, it is possible to provide a second argument in React.memo() function.

const functionalComp = (props) => {
  // component implementation
};

const areEqual = (prevProps, nextProps) => {
  // return true, this will not cause a re-render
  // return false, this will cause a re-render
};

export default React.memo(functionalComp, areEqual);

3. Write a shouldComponentUpdate method for Components

If for some reason, we do not intend to have immutable objects in state and props of our pure components, it is advisable to implement the shouldComponentUpdate method with shallow comparison. We can also combine shallowCompare and implicit update conditions in our shouldComponentUpdate method. An example for this is given below:

import shallowCompare from "react-addons-shallow-compare";

shouldComponentUpdate(nextProps, nextState) {
  const implicitComponentUpdate = nextProps.shouldUpdate;
  return shallowCompare(this, nextProps, nextState) || implicitComponentUpdate;
}

4. Avoid passing new references for unchanged data

Consider the example given below:

class UserStatus extends Component {
  render() {
    const user = { id: "525", name: "nikhil" };
    return <Status user={user} />;
  }
}

In this example, a new user object is initialized with the same value but a different reference in every render cycle of the parent component. This leads to wasted renders in the <Status /> component as the reference of user prop is changed on every re-render. We can refactor this code in multiple ways to avoid such a situation. Listing out some common practices below:

// Method 1: Declare the object in state of Parent Component
class UserStatus extends Component {
  constructor(props) {
    super(props);
    this.state = {
      user: { id: "525", name: "nikhil" },
    };
  }

  render() {
    return <Status user={this.state.user} />;
  }
}

// Method 2: If object values are constant throughout the component's lifecycle
const USER = { id: "525", name: "nikhil" };

render() {
  return <Status user={USER} />;
}

/*
    Method 3: If child object uses certain keys with values having primitive 
    data types, we can pass the values as props instead of passing the whole 
    object
    */
render() {
  return (
    <Status userId={this.state.user?.id} userName={this.state.user?.name} />
  );
}

Caution: While using Method 3, in a situation where we are unsure of the presence of certain keys, it is advisable to use optional chaining to avoid runtime crashes.

While implementing inline styling in render() function, we often write code similar to the below code snippet:

render() {
  const style = { width: "100%" };
  return <Status style={style} />;
}

render() {
  const style = [{ width: "100%" }, { height: "100%" }];
  return <Status style={style} />;
}

render() {
  return <Status style={{ width: "100%" }} />;
}

In all the above scenarios, the style prop for the <Status /> the component will have a new reference in each render cycle but values will remain unchanged. This will result in wasted renders and we can avoid inline styling in several ways. For conditional styling of components, we can use styled-components that offer more flexibility than traditional react-native stylesheet objects.

Further extending on the same idea, if we have put some null checks in place in our components and where we need to pass default values (like empty arrays or objects),  writing a code similar to below would result in wasted renders.

render() {
  return (
    <Comp
      // JSX or React.createElement creates new node reference
      element={<div>hi</div>}
      // always new reference empty array
      items={this.props.items || []}
      // always new reference for empty object
      userObj={this.props.userObj || {}}
    />
  );
}

Ensure that default props are used inside the component in such cases. Alternatively, we can move these default props outside the render function (see below example).

const defaultItems = []
const defaultUserObj = {}
const element = <div>hi</div>

render(){
  return <Comp
    element={element}
    items={this.props.items || defaultItems}
		userObj={this.props.userObj || defaultUserObj}
  />
}

5. Avoid binding values in functions inside render() function

If we have a list of items to render inside the render function, writing a code like below will cause wasted renders. This is because, in each render cycle, a new reference for the onSelect function is created and passed as a prop to <Color /> Component causing it to re-render.

render() {
  return (
    <View>
      {colorPallete.map((color) => (
        <Color onSelect={() => this.onSelectColor(color)} />
      ))}
    </View>
  );
}

Correct alternatives to writing the above code are given below:

class ParentComponent extends Component {
  constructor(props) {
    super(props);
    this.onSelectColor = this.onSelectColor.bind(this);
  }

  onSelectColor(color) {
    // implementation in parent function
  }

  render() {
    return (
      <View>
        {colorPallete.map((color) => (
          <ChildComponent onSelect={this.onSelectColor} color={color} />
        ))}
      </View>
    );
  }
}

class ChildComponent extends Component {
  handleSelect() {
    this.props.onSelect(this.props.color);
  }
}

In the above code, reference for the passed function props will remain unchanged in each render cycle of the parent component, therefore avoiding wasted renders.

That’s all, for now.

In this article, we talked about 5 ways to avoid Wasted Renders. Stay tuned for the next half of this 2 part series where we uncover the other 5 practices that helped us avoid wasted renders. We would also see the performance boost we got after we optimized renders and how it boosted our overall UX. Until then, ciao!

Nikhil Pandey

Nikhil Pandey

Senior Software Engineer, Classroom Experience Team