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.
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:
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
.
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 })
}
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
render() {
return (
- <Component button={<Button />} />
)
}
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} />);
- };
}
_renderButtons
must not changeComponent
must be re-rendered on every cycleComponent
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.
Written by Paul Armstrong who works on web applications with a couple of cats. You can follow @paularmstrong on Twitter.