Skip to main content
Published in Engineering Insights

React Component Comparison

React Component Comparison
Table of contents
  1. TL;DR
  2. What are we doing here?
  3. Ergonomics
    1. The code
    2. Gotchas
    3. Ergonomic Results
  4. Performance
    1. Expectations
    2. The Setup
    3. Results
  5. Conclusion

TL;DR#

  • Pre 16.8, class components are required for internal state
  • Functional Stateless Components are easier to read (when simple) and alleviate the worry of binding this
  • Disregarding several gotchas, use PureComponent or React.memo when you can (and, in general, make your components depend only on props and state when you can)

What are we doing here?#

We're going to compare these things:

  1. Component
  2. PureComponent
  3. Functional Stateless Components (FSCs)
  4. React.memo

in these ways:

  1. Ergonomics
  2. Performance

biased by:

  1. Me
  2. My experiences
  3. Potentially unrealistic scenarios I've concocted to demonstrate or benchmark certain features

Ergonomics#

The code#

JavaScript
class SimpleClassComponent extends React.Component {
  render() {
    return <div>{this.props.name}</div>
  }
}

class SimplePureComponent extends React.PureComponent {
  render() {
    return <div>{this.props.name}</div>
  }
}

const SimpleFsc = ({ name }) => <div>{name}</div>

const SimpleMemo = React.memo(SimpleFsc)

Component and PureComponent are pretty easy to read and nearly identical (and, pre 16.8, are your only option if you want state) but FSCs are simpler in terms of being able to avoid the this keyword and removing the class boilerplate that comes with an ugly polyfill if you're transpiling to ES5.

Gotchas#

PureComponent (or memoized FSCs) and Deeply Mutated Props

Okay this should sound like an obviously bad thing that you should try to avoid.
Consider this incredibly contrived example:

JavaScript
class Pure extends React.PureComponent {
  render() {
    return this.props.foo.bar
  }
}

class PureWrapper extends React.Component {
  constructor(props) {
    super(props)
    this.foo = { bar: 1 }
  }

  render() {
    this.foo.bar += 1
    return <Pure foo={this.foo} />
  }
}

In this case, Pure will not rerender because the prop foo hasn't changed (it's the same object in memory). This example is quite ridiculous, but you can run into this with something like Redux if you mutate your global state.

To avoid this, the React documentation suggests:

React.PureComponent's shouldComponentUpdate() only shallowly compares the objects.
If these contain complex data structures, it may produce false-negatives for deeper differences.
Only extend PureComponent when you expect to have simple props and state, or use forceUpdate() when you know deep data structures have changed.
Or, consider using immutable objects to facilitate fast comparisons of nested data.Furthermore, React.PureComponent's shouldComponentUpdate() skips prop updates for the whole component subtree.
Make sure all the children components are also "pure".

PureComponent (or memoized FSCs) Rendering Children

Having a PureComponent render children "inline" sidesteps any optimizations you would normally expect:

JavaScript
class Pure extends React.PureComponent {
  render() {
    return this.props.children
  }
}

const PureFsc = React.memo(({ children }) => children)

class PureWrapper extends React.Component {
  render() {
    return (
      <>
        <Pure>
          <SomeOtherComponent />
        </Pure>
        <PureFsc>
          <SomeOtherComponent />
        </PureFsc>
      </>
    )
  }
}

Here, <SomeOtherComponent /> is sugar for React.createElement('SomeOtherComponent') which returns a new object every time. So in Pure and PureFsc, the children prop is changing (shallowly) every time!

To avoid this, consider caching the children or creating a new pure component that renders the component and its child.

FSCs with PureComponents as Children

FSCs have a minor gotcha involving rendering PureComponent children.
If an FSC is passing a function to its PureComponent children as props, odds are it's recreating that function every render and the pure children have no way of successfully performing a strict equality on props.
Consider the following setup:

JavaScript
class Pure extends React.PureComponent {
  render() {
    return this.props.fn()
  }
}

class PureWrapper extends React.Component {
  fn = () => <div>Foo</div>
  render() {
    return <Pure fn={this.fn} />
  }
}

const PureWrapperFsc = () => {
  const fn = () => <div>Foo</div>
  return <Pure fn={fn} />
}

Here, PureWrapper, on each render, is passing the same fn to Pure so Pure, being a PureComponent, will not bother calling its render() function. PureWrapperFsc, on the other hand, creates a new function to pass to Pure every render and so Pure will also render every time.

The easiest way to avoid this problem is to use a class component, but if fn doesn't depend on props, you can define it outside your component:

JavaScript
const fn = () => <div>Foo</div>
const PureWrapperFsc = () => <Pure fn={fn} />

Ergonomic Results#

All in all, FSCs are the cleanest solution for simple components but regular React Components can have less unexpected side effects.

Performance#

"The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming." (Donald Knuth)

Now that we have that out of the way...

Expectations#

The main power of PureComponents is that they will avoid their render() function if they receive the same props and their internal state hasn't changed.
Let's check out these following examples.

Here, we're rendering a regular React Component:

impure

Notice the console indicating the component's render() function being called on each click.

Now let's see the same setup with a PureComponent:

pure

Notice how we don't see any logs for subsequent renders?
That's because PureComponents implement the shouldComponentUpdate lifecycle hook to shallowly compare their new props and state to their old props and state.
Because these haven't changed, shouldComponentUpdate() returns false and render() is not called. The previous DOM markup is instantly returned.

* Note the results are the same for FSCs vs React.memoized FSCs.

So in general, we expect big savings when using a PureComponent or React.memoized FSC and we're either rendering many components, or components whose render() function is slow, or a deep tree of components. Let's see what happens!

The Setup#

We're going to follow this repo for comparing components.
We can mess with a few things:

  1. How many components we're rendering
  2. How long each component's render function takes
  3. How many props we're passing to each component
  4. If components are receiving the same props on subsequent renders

For the following comparisons:

  1. 1000 components are rendered
  2. "Type" indicates which type of component (listed above) we're rendering
  3. "Complexity" indicates how long each component takes to render. Simple ~ 0.25ms per component. Complex ~ 1.2ms per component (note in the repo this corresponds to the 250th prime for me)
  4. "Prop Count" indicates how many props are passed to each component. Props are simple numbers.
  5. Render times are in ms. We record performance.now as part of the setState that starts the render and then in componentDidMount. They are an average over 10 renders
  6. (mutating) indicates that one prop changes for each rendered component each render
  7. (non-mutating) indicates that components receive the exact same props each render
Type Complexity Prop Count Time (mutating) Time (non-mutating)
Component Simple 1 23 23
Component Simple 1000 620 610
Component Complex 1 670 660
Component Complex 1000 1250 1250
FSC Simple 1 22 21
FSC Simple 1000 610 600
FSC Complex 1 600 600
FSC Complex 1000 1180 1170
Pure Simple 1 22 1.2
Pure Simple 1000 740 740
Pure Complex 1 600 1.2
Pure Complex 1000 1320 740
React.memo Simple 1 22 0.83
React.memo Simple 1000 730 750
React.memo Complex 1 540 1.0
React.memo Complex 1000 1260 740

Results#

As expected, the render times for non-pure Components are nearly identical regardless of props mutation.
For PureComponent and React.memo, we see huge performance savings when rendering lots of complex components whose props aren't changing.
That makes sense. If render() takes a long time and we can skip all that work, we'll see big gains.
We also see a slight performance boost when choosing functional components over class components.The only surprising situation here occurs when passing lots of props.
It takes roughly 600ms just to pass 1000 props to 1000 children, never mind shallowly compare them.
Given that, we do see a slight performance hit when using PureComponent and React.memo because the work of prop-equality-checking is more than the work of render().

Conclusion#

  • If you are seeing performance issues related to unnecessary renders you might get some easy wins with PureComponent and React.memo when properly applied.
  • If your component is always receiving new props, pure component types could waste time doing unnecessary prop equality checks, but it likely won't be noticeable.

Learn more about how The Gnar builds React  applications.

Frequently asked questions

Why can a deeply changed prop fail to trigger a render?

PureComponent compares props shallowly. In the example, a nested value changes but its containing object remains the same object, so the component does not rerender.

Why can inline children defeat the optimization?

Creating a child element returns a new object on each render. That makes the children prop change each time. The author suggests caching the children or creating a pure component that renders both parent and child.

Why can passing a function cause repeated renders?

A functional component may create a new function on every render. Its pure child then receives a different prop. If the function does not depend on props, the author suggests defining it outside the component.

When might pure components add unnecessary work?

If a component always receives new props, equality checks can be wasted work. The benchmark also shows that checking many props can cost more than a simple render.

Stay In The Loop

Never miss an update. Get the latest blogs, webinars, and expert insights straight to your inbox from The Gnar.