Techniques to optimize React render performance: part 2

useanvil

Anvil Engineering

Posted on March 11, 2022

Techniques to optimize React render performance: part 2

This is the final part in a two part series on optimizing React component render performance in your UI. In part one of optimizing React performance, we covered tooling, profiling, and generally tracking down exactly where your UI is slow. If you haven't read it yet, check it out. Part 1 was trying to answer Where is it slow? and Why is it slow? Like debugging, knowing exactly where you need to spend your time will make the solution come a lot easier.

By now you should have some UI profiling under your belt and have a good idea of which components are slow. It is high time to fix them. In this post, we will focus on just that: techniques and pitfalls to improve your slow React components.

Render less

The central tenet of improving performance in general is effectively: "do less work." In React land, that usually translates into rendering less often. One of the initial promises of React and the virtual DOM was that you didn't need to think very hard about rendering performance: slowness is caused by updates to the Real DOM, and React abstracts the Real DOM from you in a smart way. Diffing of the virtual DOM and only updating the necessary elements in the Real DOM will save you.

In UIs with a lot of components, the reality is that you still need to be concerned with how often your components are rendering. The less DOM diffing React needs to do, the faster your UI will be. Do less work, render less often. This will be the focus of our initial performance efforts.

Example: list of fields

We'll be applying several different optimization techniques to the same example: a list of webform fields. We'll pretend that we've identified this part of the UI as something to optimize. This same example was used in our first React performance post and we identified a couple issues:

  • When the list re-renders with a lot of fields, it feels slow.
  • Each field in the list renders too often; we only want fields that have changed to re-render.

Example list of webform fields

A simplified version of the code and a basis for our optimization work:

// Each individual field
const Field = ({ id, label, isActive, onClick }) => (
  <div onClick={onClick} className={isActive ? 'active' : null}>
    {label}
  </div>
)

// Renders all fields
const ListOfFields = ({ fields }) => {
  // Keep track of the active field based on which one
  // was clicked last
  const [activeField, setActiveField] = useState(null)

  return (
    <div>
      {fields.map(({ id, label }) => (
        <Field
          id={id}
          label={label}
          isActive={id === activeField}
          onClick={() => setActiveField(id)}
        />
      ))}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Our example for techniques in this post

Note that we are keeping track of an active field in ListOfFields. Each time a Field is clicked, it will store the last-clicked Field's id in the ListOfFields state. The state change will trigger ListOfFields to re-render.

By default, when ListOfFields re-renders, all of the child Field components will re-render as well. For example, clicking one Field will set activeField state in ListOfFields which will cause a ListOfFields re-render. The parent re-render will cause all of the child Field components to re-render. Every one of them! Every time!

Solutions

Our potential solutions will center around two main goals:

  1. Render child Field components less often
  2. Compute expensive operations in the render function less often

After this post, you should be able to apply all these techniques to your own codebase while avoiding the pitfalls. Here's what we'll be covering:

Let's dig in!

Pure components

The first potential solution to selective component re-rendering is converting our Field component into a pure component. A pure component will only re-render if the component's props change. There are caveats, of course, but we'll get to those in a minute.

In our example above, when a Field is clicked and the activeField state is set, all Field components are re-rendered. Not good! The ideal scenario is that only two Field components are re-rendered: the previously-active and the newly-active Fields. It should skip rendering all of the other Fields that did not change.

Pure components are extremely easy to use. Either:

  • Wrap a functional component with React.memo
  • Or define your class component with React.PureComponent instead of React.Component
import React from 'react'

// These components will only re-render
// when their props change!

// Pure functional component
const Field = React.memo(({ id, label, isActive, onClick }) => (
  <div onClick={onClick}>
    {label}
  </div>
))

// Pure class component
class Field extends React.PureComponent {
  render () {
    const { id, label, isActive, onClick } = this.props
    return (
      <div onClick={onClick}>
        {label}
      </div>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

Using pure components can be an easy win, but it is also very easy to shoot yourself in the foot and unknowingly break re-render prevention.

The big caveat is that a pure component's props are shallow-compared by default. Basically, if (newProps.label !== oldProps.label) reRender(). This is fine if all of your props are primitives: strings, numbers, booleans. But things get more complicated if you are passing anything else as props: objects, arrays, or functions.

Pure component pitfall: callback functions

Here is our original example with Field as a pure component. Turns out even in our new example using pure components, the re-rendering issue has not improved—all Field components are still being rendered on each ListOfFields render. Why?

// Still re-renders all of the fields :(
const Field = React.memo(({ id, label, isActive, onClick }) => (
  <div onClick={onClick}>
    {label}
  </div>
))

const ListOfFields = ({ fields }) => {
  const [activeField, setActiveField] = useState(null)
  return (
    <div>
      {fields.map(({ id, label }) => (
        <Field
          id={id}
          label={label}
          isActive={id === activeField}
          onClick={() => setActiveField(id)} // Problem!!!
        />
      ))}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

The issue is that the onClick callback function is being created in the render function. Remember that pure components do a shallow props comparison; they test equality by reference, but two onClick functions are not equal between renders: (() => {}) === (() => {}) is false.

How can we fix this? By passing the same function to onClick in each re-render. You have a couple options here:

  1. Pass in setActiveField directly
  2. Wrap your callback in the useCallback hook
  3. Use bound member functions when using class components

Here the issue is fixed with the first two options in a functional component:

const ListOfFields = ({ fields }) => {
  // The useState hook will keep setActiveField the same
  // shallow-equal function between renders
  const [activeField, setActiveField] = useState(null)
  return (
    <div>
      {fields.map(({ id, label }) => (
        <Field
          id={id}
          label={label}
          isActive={id === activeField}

          // Option 1: setActiveField does not change between renders,
          // you can pass it directly without breaking React.memo
          onClick={setActiveField}

          // Option 2: memoize the callback with useCallback
          onClick={useCallback(() => setActiveField(id), [id])}
        />
      ))}
    </div>
  )
}

// An anonymous function in the render method here will not
// trigger additional re-renders
const Field = React.memo(({ id, label, isActive, onClick }) => (
  <div
    // Option 1: Since setActiveField is passed in directly,
    // we need to give it an id. An inline function here is ok
    // and will not trigger re-renders
    onClick={() => onClick(id)}

    // Option 2: Since the id is passed to the setActiveField
    // in the parent component, you can use the callback directly
    onClick={onClick}
  >
    {label}
  </div>
))
Enter fullscreen mode Exit fullscreen mode

And a fix using class components:

class Field extends React.PureComponent {
  handleClick = () => {
    const { id, onClick } = this.props
    onClick(id)
  }

  render () {
    const { label, isActive } = this.props
    return (
      <div onClick={this.handleClick}>
        {label}
      </div>
    )
  }
}

class ListOfFields extends React.Component {
  state = { activeField: null }

  // Use a bound function
  handleClick = (activeField) => {
    this.setState({ activeField })
  }

  render () {
    const { fields } = this.props
    return (
      <div>
        {fields.map(({ id, label }) => (
          <Field
            id={id}
            label={label}
            isActive={id === this.state.activeField}

            // Solved! The bound function does not change between renders
            onClick={this.handleClick}
          />
        ))}
      </div>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

Pure component pitfall: dynamic data in the render function

The function callback pitfall described above is really a subset of a larger issue: passing props dynamically created in the render function. For example, because { color: 'blue' } is defined in the render function here, it will be different on each render, which will force a re-render on every Field component.

// Pure component for each individual field
const Field = React.memo(({ label, style }) => (
  <div style={style}>{label}</div>
))

const ListOfFields = ({ fields }) => {
  const style = { color: 'blue' } // Problem!
  return fields.map(({ label }) => (
    <Field
      label={label}
      style={style}
    />
  ))
}
Enter fullscreen mode Exit fullscreen mode

The ideal solution is to create the style prop's object somewhere outside of the render function. If you must dynamically create an object or array in the render function, the created object can be wrapped in the useMemo hook. The useMemo hook is covered in the caching computed values section below.

shouldComponentUpdate

By default, pure components shallow-compare props. If you have props that need to be compared in a more complex way, there is a shouldComponentUpdate lifecycle function for class components and a functional / hooks equivalent in React.memo.

For the functional implementation, React.memo takes a second param: a function to do the props comparison. It's still beneficial to shoot for props that do not change between renders unless a re-render is necessary, but the real world is messy and these functions provide an escape hatch.

const Field = React.memo(({ label, style }) => (
  <div style={style}>{label}</div>
), (props, nextProps) => (
  // Return true to NOT re-render
  // We can shallow-compare the label
  props.label === nextProps.label &&
    // But we deep compare the `style` prop
    _.isEqual(props.style, nextProps.style)
))
Enter fullscreen mode Exit fullscreen mode

Then implemented as a class component

class Field extends React.Component {
  shouldComponentUpdate () {
    // Return false to NOT re-render
    return props.label !== nextProps.label ||
      // Here we deep compare style
      !_.isEqual(props.style, nextProps.style)
  }

  render () {
    const { label, style } = this.props
    return (
      <div style={style}>{label}</div>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

Caching computed values

Let's say that while profiling your app you've identified an expensive operation happening on each render of ListOfFields:

const ListOfFields = ({ fields, filterCriteria }) => {
  const [activeField, setActiveField] = useState(null)

  // This is slow!
  const filteredFields = verySlowFunctionToFilterFields(fields, filterCriteria)

  return filteredFields.map(({ id, label }) => (
    <Field
      id={id}
      label={label}
      isActive={id === activeField}
      onClick={setActiveField}
    />
  ))
}
Enter fullscreen mode Exit fullscreen mode

In this example, every time a Field is clicked, it will re-run verySlowFunctionToFilterFields. But it doesn't need to! The filteredFields only need to be computed each time either the fields or filterCriteria are changed. You can wrap your slow function in the useMemo() hook to memoize filteredFields. Once it's memoized, verySlowFunctionToFilterFields will only re-run when fields or filterCriteria changes.

import React, { useMemo } from 'react'

const ListOfFields = ({ fields, filterCriteria }) => {
  const [activeField, setActiveField] = useState(null)

  // Better, yay
  const filteredFields = useMemo(() => (
    verySlowFunctionToFilterFields(fields, filterCriteria)
  ), [fields, filterCriteria])

  return filteredFields.map(({ id, label }) => (
    <Field
      id={id}
      label={label}
      isActive={id === activeField}
      onClick={setActiveField}
    />
  ))
}
Enter fullscreen mode Exit fullscreen mode

Like pure components, you need to be careful that you do not break the comparison. useMemo suffers from the same pitfalls as pure components: it performs a shallow-comparison of arguments. That means if fields or filterCriteria are re-created between renders, it will still re-compute your expensive operation on each render.

Unfortunately useMemo does not accept a second comparison argument like React.memo. If you want to do a deep comparison there are several code samples and libraries out there you can use.

Using useMemo to limit re-renders

In our pure component pitfalls above, we noted that passing objects created in the render function can break a pure component's benefits. Note here that the style object is being created on each render of ListOfFields, forcing all Fields to render all the time.

// Pure component for each individual field
const Field = React.memo(({ label, style }) => (
  <div style={style}>{label}</div>
))

const ListOfFields = ({ fields }) => {
  const style = { color: 'blue' } // Problem! Forces Field to always re-render
  return fields.map(({ label }) => (
    <Field
      label={label}
      style={style}
    />
  ))
}
Enter fullscreen mode Exit fullscreen mode

While the ideal scenario is to move creation of the style object out of the render function, sometimes creating an object in the render function is necessary. In those instances, useMemo can be helpful:

const ListOfFields = ({ color, fields }) => {
  // This will be cached until the `color` prop changes
  const style = useMemo(() => ({ color }), [color])
  return fields.map(({ label }) => (
    <Field
      label={label}
      style={style}
    />
  ))
}
Enter fullscreen mode Exit fullscreen mode

Caching computed values in class components

Caching computed values in class components is a bit clunkier, especially if you are trying to avoid the UNSAFE_componentWillReceiveProps() lifecycle function. The React maintainers recommend using the memoize-one library:

import React from 'react'
import memoize from "memoize-one"

class ListOfFields extends React.Component {
  state = { activeField: null }

  handleClick = (id) => this.setState({activeField: id})

  getFilteredFields = memoize(
    (fields, filterCriteria) => (
      verySlowFunctionToFilterFields(fields, filterCriteria)
    )
  )

  render () {
    const { fields, filterCriteria } = this.props
    const filteredFields = this.getFilteredFields(fields, filterCriteria)
    return filteredFields.map(({ id, label }) => (
      <Field
        id={id}
        label={label}
        isActive={id === activeField}
        onClick={this.handleClick}
      />
    ))
  }
}
Enter fullscreen mode Exit fullscreen mode

Consider your architecture

So far, we've focused on pretty tactical solutions: e.g. use this library function in this way. A much broader tool in your toolbox is adjusting your application's architecture to re-render fewer components when things change. At the very least, it is helpful to understand how your app's data flow and data locality affects performance.

A couple questions to answer: at what level are you storing application state? When something changes deep in the component tree, where is the new data stored? Which components are being rendered when state changes?

In the spirit of our webform example, consider the following component tree:

<Application>
  <Navbar />
  <AnExpensiveComponent>
    <ExpensiveChild />
  </AnExpensiveComponent>
  <Webform>
    <ListOfFields>
      <Field />
      <Field />
      <Field />
    </ListOfFields>
  </Webform>
<Application>
Enter fullscreen mode Exit fullscreen mode

For the webform editor, we need an array of fields stored somewhere in this tree. When a field is clicked or label is updated, the array of fields needs to be updated, and some components need to be re-rendered.

Let's say at first we keep the fields state in the <Application /> Component. When a field changes, the newly changed field will bubble up all the way to the Application component's state.

const Application = () => {
  const [fields, setFields] = useState([{ id: 'one'}])
  return (
    <>
      <Navbar />
      <AnExpensiveComponent />
      <Webform fields={fields} onChangeFields={setFields} />
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

With this architecture, every field change will cause a re-render of Application, which will rightly re-render Webform and all the child Field components. The downside is that each Field change will also trigger a re-render of Navbar and AnExpensiveComponent. Not ideal! AnExpensiveComponent sounds slow! These components do not even care about fields, why are they being unnecessarily re-rendered here?

A more performant alternative would be to store the state closer to the components that care about the fields array.

const Application = () => (
  <>
    <Navbar />
    <AnExpensiveComponent />
    <Webform />
  </>
)

const Webform = () => {
  const [fields, setFields] = useState([{ id: 'one'}])
  return (
    <ListOfFields fields={fields} onChangeFields={setFields} />
  )
}
Enter fullscreen mode Exit fullscreen mode

With this new setup, Application, Navbar, and AnExpensiveComponent are all blissfully unaware of fields. Don't render, ain't care.

In practice: Redux

While I am not a Redux advocate, it really shines in this scenario. The Redux docs even outline this as the number one reason to use Redux:

You have large amounts of application state that are needed in many places in the app.

"Many places in the app" is the key for us here. Redux allows you to connect() any component to the Redux store at any level. That way, only the components that need to will re-render when the requisite piece of state changes.

// Application does not need to know about fields
const Application = () => (
  <>
    <Navbar />
    <AnExpensiveComponent />
    <ListOfFields />
  </>
)


// ListOfFieldsComponent does need to know about
// fields and how to update them
const ListOfFieldsComponent = ({ fields, onChangeFields }) => (
  fields.map(({ label, onChangeFields }) => (
    <Field
      label={label}
      style={style}
      onChange={eventuallyCallOnChangeFields}
    />
  ))
)

// This will connect the Redux store only to the component
// where we need the state: ListOfFields
const ListOfFields = connect(
  (state) => ({ fields: state.fields }),
  (dispatch) => {
    onChangeFields: (fields) => dispatch({
      type: 'CHANGE_FIELDS',
      payload: fields
    }),
  }
)(ListOfFieldsComponent)
Enter fullscreen mode Exit fullscreen mode

If you're using Redux, it's worth checking which components are being connected to which parts of the store.

App state best practices?

Deciding where to put your application state, or pieces of your application state is tricky. It depends heavily on what data you are storing, how it needs to be updated, and libraries you're using. In my opinion, there are no hard / fast rules here due to the many tradeoffs.

My philosophy is to initially optimize for consistency and developer reasonability. On many pages, it doesn't matter where the state is, so it makes the most sense to keep the ugly bits in one place. State is where the bugs are, premature optimization is the root of all evil, so for the sake of our own sanity let's not scatter state around if we can help it.

For example, your company's about page can have all data come into the top-level component. It's fine, and is likely more ideal for developer UX. If performance is an issue for some component, then it's time to think deeper about the performance of your app's state flow and maybe break the paradigm for performance reasons.

At Anvil, we use Apollo to store app state from the API, and mostly adhere to the Container pattern: there is a "Container" component at a high level doing the fetching + updating via the API, then "Presentational" component children that consume the data as props. To be a little more concrete:

  • Our app's pages all start out with all data for a page being fetched and stored at the Route level.
  • For complex components with a lot of changes to state, we store state at the deepest level that makes sense.
  • We store ephemeral UI state like hover, 'active' elements, modal visibility, etc., as deep as possible.

This is how we approach things, but your organization is likely different. While your approach and philosophical leanings may be different, it's helpful to understand that the higher the state is in the component tree, the more components React will try to re-render. Is that an issue? If so, what are the tools to fix it? Those are hard questions. Hopefully the sections above can help give you a bit of direction.

Other potential solutions

The options covered in the meat of this post can help solve many of your performance ills. But of course they not the end-all to react performance optimization. Here are a couple other quick potential solutions.

Debouncing

The most important thing to a user is perceived speed. If your app does something slow when they aren't looking, they don't care. Debouncing is a way to improve perceived speed, i.e. it helps you move some actual work away from a critical part of a user interaction.

A debounced function will ratelimit or group function calls into one function call over some time limit. It's often used to limit events that happen frequently in quick succession, for example keydown events or mousemove events. In those scenarios, instead of doing work on each keystroke or mouse event, it would call your event handler function when a user has stopped typing, or has stopped moving the mouse for some amount of time.

Here's an example using lodash debounce:

import _ from 'lodash'

function handleKeyDown () {
  console.log('User stopped typing!')
}

// Call handleKeyDown if the user has stopped
// typing for 300 milliseconds
const handleKeyDownDebounced = _.debounce(
  handleKeyDown,
  300
)

<input onKeyDown={handleKeyDownDebounced} />
Enter fullscreen mode Exit fullscreen mode

Rendering very large lists of elements

Do you need to render several hundred or thousands of items in a list? If so, the DOM itself might be the bottleneck. If there are a very large number of elements in the DOM, the browser itself will slow. The technique to solve for this situation is a scrollable list where only the items visible to the user are rendered to the DOM.

You can leverage libraries like react-virtualized or react-window to handle this for you.

You made it!

Performance optimization is tricky work; it is filled with tradeoffs and could always be better. Hopefully this post helped add tools to your performance optimization toolbox.

Before we depart, I want to stress the importance of profiling your UI before applying any of these techniques. You should have a really good idea of which components need to be optimized before digging in. Performance optimization often comes at the expense of readability and almost always adds complexity.

In some cases, blindly adding performance optimizations could actually make your UI slower. For example, it may be tempting to make everything a pure component. Unfortunately that would add overhead. If everything is a pure component, React will be doing unnecessary work comparing props on components that do not need it. Performance work is best applied only to the problem areas. Profile first!

Do you have any feedback? Are you developing something cool with PDFs or paperwork automation? Let us know at developers@useanvil.com. We’d love to hear from you!

💖 💪 🙅 🚩
useanvil
Anvil Engineering

Posted on March 11, 2022

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

Sign up to receive the latest update from our blog.

Related