This web dev stuff is hard

Prop type performance considerations for React

January 03, 2019

Rendering large trees of components in React can be expensive. When the state managed by Redux changes, it triggers updates to the main component tree and on down through all of the sub-trees. When state updates often, this can start to slow down your application while it needs to re-render every component.

To prevent this from happening, you might make use of PureComponent and shouldComponentUpdate. While each of these is a strategy for managing updates to components and their sub-trees, there are certain patterns that can slip by and not be caught appropriately.

It is also important to understand that using PureComponent is typically the goal, whenever possible, since it is optimized at React’s library level. Custom shouldComponentUpdate implementations are encouraged when PureComponent may not be possible (e.g. an entire data structure object is required as a prop to render a component, like a Tweet object).

Note: I found this in some archives of half-finished blog posts written sometime between 2015 and 2018. Some items may be a bit dated, the patterns not super new, but I hope that the lessons and ideas are helpful either way.

New Arrays and Objects

Most components use (or should use) shallow comparisons against their props and state during shouldComponentUpdate (or in PureComponent) in order to determine whether or not they need to be updated and re-rendered.

When a property is an object or array, every time a new instance of one is passed in, it will fail shallow comparison and trigger an update. Take the following scenarios:

Static data

  render() {
    return (
-     <Component locationState={{ foo: 'bar' }} />
    );
  }

In the above case, every time render is called, a new object with the same key/value pairs will be sent in every time. This is unnecessary, since the data is static. It can easily be alleviated by hoisting the static data outside of any lifecycle.

+ const locationState = { foo: 'bar' };

// ...

  render() {
    return (
+     <Component locationState={locationState} />
    );
  }

Now, this.props.locationState will always be stricly equal (===) to nextProps.locationState and not trigger an update/re-render when using a PureComponent.

Building from instance props or state

  render() {
-   const { item1, item2 } = this.props;
    return (
-     <Component data={[ item1, item2 ]} />
    );
  }

In this case, a sub-component is dependent on the current component’s props and state. It may not be possible to prevent rendering at the parent level, so we need another solution to avoid creating a new array instance of these properties if they haven’t changed.

+ import createInstanceSelector from 'modules/create-instance-selector';

// ...

  render() {
    return (
+     <Component data={this.getData()} />
    );
  }

+ getData = createInstanceSelector(
+   (props, state) => props.item1,
+   (props, state) => props.item2,
+   (item1, item2) => [ item1, item2 ]
+ );

We have a module called createInstanceSelector for this purpose. It works like reselect’s selectors, in that it memoizes composable, derived data. In the above case, this.getData() will only return a new array if either props.item1 or props.item2 has changed since the last call. Here’s an example of how you might implement that:

import createSelector from 'reselect'

export default (componentInstance, ...selectors) => {
  const instanceSelector = createSelector(...selectors)
  return (instance = componentInstance) =>
    instanceSelector(instance.props, instance.state, { ...instance })
}

New Functions

Similar to arrays and objects, every time a function is defined, it will fail strict equality checks. There are multiple ways to ensure you don’t create a new function on each render cycle. Pick the one that works best for you.

  render() {
    return (
-     <Component identityFunction={(item) => item.id} />
    );
  }
+ const identityFunction = (item) => item.id;
  // ...
  render() {
    return (
+     <Component identityFunction={identityFunction} />
    );
  }

Tip: You can use the eslint-plugin-react/jsx-no-bind rule to disallow the undesired pattern

Components

  render() {
    return (
-     <Component button={<Button />} />
    )
  }

Functions that render sub-trees

  class ParentComponent extends Component {
    render() {
      return (
-       <Component renderButtons={this._renderButtons}
      );
    }

-   _renderButtons = () => {
-     const { items } = this.props;
-     return items.map((item, i) => <Item data={item} key={i} />);
-   };
  }

When to use this pattern

  • When the output of _renderButtons must not change
  • When Component must be re-rendered on every cycle

When not to use this pattern

  • When Component is a PureComponent

Keep in mind, performance is almost always application specific. When you use PureComponent or shouldComponentUpdate are up to your needs. Take everything here as a starting guideline and not an end-all, be-all.


Paul Armstrong

Written by Paul Armstrong who works on web applications with a couple of cats. You can follow @paularmstrong on Twitter.