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.
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>
)
}
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:
- Render child
Field
components less often - 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:
- Pure components
- shouldComponentUpdate
- Caching computed values
- Consider your architecture
- Other solutions
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 Field
s. 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 ofReact.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>
)
}
}
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>
)
}
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:
- Pass in
setActiveField
directly - Wrap your callback in the
useCallback
hook - 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>
))
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>
)
}
}
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}
/>
))
}
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)
))
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>
)
}
}
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}
/>
))
}
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}
/>
))
}
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 Field
s 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}
/>
))
}
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}
/>
))
}
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}
/>
))
}
}
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>
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} />
</>
)
}
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} />
)
}
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)
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} />
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!
Top comments (0)