by Mark Lodato
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
orReact.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:
in these ways:
- Ergonomics
- Performance
biased by:
- Me
- My experiences
- Potentially unrealistic scenarios I've concocted to demonstrate or benchmark certain features
Ergonomics
The code
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:
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
’sshouldComponentUpdate()
only shallowly compares the objects.
If these contain complex data structures, it may produce false-negatives for deeper differences.
Only extendPureComponent
when you expect to have simple props and state, or useforceUpdate()
when you know deep data structures have changed.
Or, consider using immutable objects to facilitate fast comparisons of nested data.Furthermore,
React.PureComponent
’sshouldComponentUpdate()
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:
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:
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 prop
s, you can define it outside your component:
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 PureComponent
s 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:
Notice the console indicating the component's render()
function being called on each click.
Now let's see the same setup with a PureComponent
:
Notice how we don't see any logs for subsequent renders?
That's because PureComponent
s 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.memo
ized FSCs.
So in general, we expect big savings when using a PureComponent
or React.memo
ized 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:
- How many components we're rendering
- How long each component's render function takes
- How many props we're passing to each component
- If components are receiving the same props on subsequent renders
For the following comparisons:
- 1000 components are rendered
- "Type" indicates which type of component (listed above) we're rendering
- "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)
- "Prop Count" indicates how many props are passed to each component. Props are simple numbers.
- 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 - (mutating) indicates that one prop changes for each rendered component each render
- (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 (identified using tools like why-did-you-update) you might get some easy wins with
PureComponent
andReact.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.