- React is Slow, React is Fast: Optimizing React Apps in Practice
- Francois Zaninotto
- The Nuggets translation Project
- Translator: Jiang Haichao
- Proofreader: Wneil, Chen Lu
React is slow. I mean, any medium-sized React app is slow. But before you start looking for alternatives, you should understand that any mid-sized Angular or Ember application is also slow. The good news: If you care about performance, making React apps super fast is pretty easy. This article is a case study.
Measuring React Performance
What exactly do I mean by “slow”? Let me give you an example.
I’m working on admin-on-Rest, an open source project that uses Material – UI and Redux to provide an Admin user graphical interface for any REST API. The application already has a data page that presents a series of records in a table. I wasn’t satisfied with how responsive the interface was when the user changed order, navigated to the next page, or filtered results. The following screenshot is the result of the refresh slowing down the 5x.
So let’s see what happens. I insert one in the URL, okay? React_perf. Since React 15.4, component Profiling can be enabled through this property. Wait for the initialization data page to load. Open the Timeline TAB in Chrome Developer Tools, click the “Record” button, and click the table header to update the order. Once the data is updated, click the “Record” button again to stop recording, and Chrome will display a yellow flame chart under the “User Timing” TAB.
If you’ve never seen a fire chart before, it looks a little scary, but it’s actually very easy to use. This “User Timing” diagram shows how much time each component takes. It hides the amount of time being spent inside React (which you can’t tune), so this diagram lets you focus on optimizing your application. The Timeline shows screenshots of Windows at different stages, which focuses on the point in time when the header was clicked.
It seems to have been rerendered after clicking the sort button, even before getting the REST data, and my application rerendered the
component. This process took more than 500ms. The app just updates the sorting icon of the table header and displays a gray mask over the table to indicate that the data is still being transferred.
In addition, the app takes half a second to provide visual feedback on clicks. 500ms is definitely perceptible – UI experts say that user perception is instantaneous when the visual layer changes less than 100ms. This perceptible change is what I call “slow”.
Why is it updated?
According to the above flame diagram, you will see many small indentations. That’s not a good sign. This means that many components have been redrawn. The flame chart shows that the
component took the most time to update. Why does the application redraw the entire table before retrieving new data? Let’s dig deeper.
To understand why a redraw is usually done by adding console.log() to the render function. Because of functional components, you can use a single-line high-order component (HOC) like this:
// in src/log.js const log = BaseComponent => props => { console.log(`Rendering ${BaseComponent.name}`); return <BaseComponent {... props} />; } export default log; // in src/MyComponent.js import log from './log'; export default log(MyComponent);
Copy the code
Tip: Another noteworthy React performance tool is why-did-you-update. The NPM package builds on React with a patch that gives a console warning when a component is redrawn based on the same props. Note: The output is verbose and does not work in functional components.
In this example, when the user clicks on the column title, the application triggers an action to change state: the column’s sort [currentSort] is updated. This state change triggers a redraw of the
page, which in turn causes the entire
component to be redrawn. After clicking the sort button, we want the Datagrid header to be redrawn immediately as feedback for user behavior.
It’s not usually a single slow component (represented as a large block in the fire diagram) that makes React slow. Most of the time, what slows React applications down is the useless redrawing of many components. You may have read that the React virtual DOM is super fast. That’s true, but in a medium-sized application, a full redraw can easily result in hundreds of components being redrawn. Even the fastest virtual DOM templatingengines can’t get this process below 16ms.
Here’s the render() method of the
component:
// in Datagrid.js
render() {
const { resource, children, ids, data, currentSort } = this.props;
return (
<table>
<thead>
<tr>
{React.Children.map(children, (field, index) => (
<DatagridHeaderCell key={index} field={field} currentSort={currentSort} updateSort={this.updateSort}
/>
))}
</tr>
</thead>
<tbody>
{ids.map(id => (
<tr key={id}>
{React.Children.map(children, (field, index) => (
<DatagridCell record={data[id]} key={`${id}-${index}`} field={field} resource={resource} />
))}
</tr>
))}
</tbody>
</table>
);
}
Copy the code
This seems like a very simple implementation of the Datagrid, but it’s very inefficient. Each
call renders at least two or three components. As you can see from the initial screen shot, the table has 7 columns and 11 rows, so 7x11x3 = 231 components will be re-rendered. This is a waste of time when it’s just a change to currentSort. Although React does not update the real DOM without updating the virtual DOM, processing of all components takes up to 500ms.
To avoid useless body rendering, the first step is to extract it:
// in Datagrid.js render() { const { resource, children, ids, data, currentSort } = this.props; return ( <table> <thead> <tr> {React.Children.map(children, (field, index) => ( <DatagridHeaderCell key={index} field={field} currentSort={currentSort} updateSort={this.updateSort} /> ))} </tr> </thead> <DatagridBody resource={resource} ids={ids} data={data}> {children} </DatagridBody> </table> ); ) ; }
Copy the code
By extracting the table body logic, I create a new
component:
// in DatagridBody.js
import React from 'react';
const DatagridBody = ({ resource, ids, data, children }) => (
<tbody>
{ids.map(id => (
<tr key={id}>
{React.Children.map(children, (field, index) => (
<DatagridCell record={data[id]} key={`${id}-${index}`} field={field} resource={resource} />
))}
</tr>
))}
</tbody>
);
export default DatagridBody;
Copy the code
Extracting table bodies has no effect on performance, but it reflects a path to optimization. Large, generic components are difficult to optimize. Small, single-responsibility components are easier to handle.
shouldComponentUpdate
The React documentation has a very specific method for avoiding useless redraws: shouldComponentUpdate(). By default, React always redraws components into the virtual DOM. In other words, as the developer, it’s your job to check the props components that haven’t changed and skip the drawing in that case.
In the example of the
component above, the body should not be redrawn unless props changes.
So the components should look like this:
import React, { Component } from 'react'; class DatagridBody extends Component { shouldComponentUpdate(nextProps) { return (nextProps.ids ! == this.props.ids || nextProps.data ! == this.props.data); } render() { const { resource, ids, data, children } = this.props; return ( <tbody> {ids.map(id => ( <tr key={id}> {React.Children.map(children, (field, index) => ( <DatagridCell record={data[id]} key={`${id}-${index}`} field={field} resource={resource} /> ))} </tr> ))} </tbody> ); } } export default DatagridBody;
Copy the code
Tip: Instead of implementing the shouldComponentUpdate() method manually, I can inherit the React PureComponent instead of the Component. This component compares all props with strict equivalence (===) and only redraws when any of the props changes. But I know that resource and children do not change in the context of the example, so there is no need to check their equivalence.
With this optimization, the redrawing of the
component skips the body of the table and all 231 components after the header is clicked. This reduces the update time from 500ms to 60ms. Network performance improved over 400ms!
Tip: don’t be fooled by the width of the fire map, it’s bigger than the previous one. This flame chart shows the performance is definitely the best!
The shouldComponentUpdate optimization removed many potholes from the diagram and reduced overall rendering time. I would use the same method to avoid more repainting (e.g., avoid redrawing sidebar, action buttons, unchanged header and page number). After an hour of work, after clicking the header column, the entire page takes only 100ms to render. That’s pretty fast – even though there’s still room for optimization.
Adding a shouldComponentUpdate method may seem like a hassle, but if you really care about performance, most of the components you write should be added.
Don’t add shouldComponentUpdate everywhere – Implementing shouldComponentUpdate on simple components is sometimes more time-consuming than just rendering components. Don’t use it early in the app either – this will be optimized too early. But add shouldComponentUpdate logic to keep running fast as your application grows and you find performance bottlenecks on components.
restructuring
I wasn’t very happy with my previous work on
: due to the use of shouldComponentUpdate, I had to make it a simple class-based functional component. This adds many lines of code, and each line takes effort – to write, debug, and maintain.
Fortunately, thanks to Recompose, you can implement shouldComponentUpdate logic on HOC. It is a functional tool for React that provides high-order instances of Pure ().
// in DatagridBody.js
import React from 'react';
import pure from 'recompose/pure';
const DatagridBody = ({ resource, ids, data, children }) => (
<tbody>
{ids.map(id => (
<tr key={id}>
{React.Children.map(children, (field, index) => (
<DatagridCell record={data[id]} key={`${id}-${index}`} field={field} resource={resource} />
))}
</tr>
))}
</tbody>
);
export default pure(DatagridBody);
Copy the code
The only difference from the original implementation above is that I exported pure(DatagridBody) instead of DatagridBody. Pure is like PureComponent, but without the extra class template.
When using shouldUpdate() for recompose instead of pure(), I can be even more specific, targeting only the props I know could change:
// in DatagridBody.js import React from 'react'; import shouldUpdate from 'recompose/shouldUpdate'; const DatagridBody = ({ resource, ids, data, children }) => ( ... ) ; const checkPropsChange = (props, nextProps) => (nextProps.ids ! == this.props.ids || nextProps.data ! == this.props.data); export default shouldUpdate(checkPropsChange)(DatagridBody);
Copy the code
CheckPropsChange is a pure function that I can even export for unit testing.
The Recompose library provides more HOC performance optimizations, such as onlyUpdateForKeys(), which does exactly the same checks as the checkPropsChange I wrote myself.
// in DatagridBody.js import React from 'react'; import onlyUpdateForKeys from 'recompose/onlyUpdateForKeys'; const DatagridBody = ({ resource, ids, data, children }) => ( ... ) ; export default onlyUpdateForKeys(['ids', 'data'])(DatagridBody);
Copy the code
The Recompose library is highly recommended. In addition to optimizing performance, it helps you extract data retrieval logic, HOC composition, and props in a functional and measurable manner.
If you’re using Redux to manage your application’s state (which I recommend), the Connected component is already a pure component. No need to add HOC. Just remember that as soon as one of the props changes, the Connected component is redrawn – this includes all of the child components as well. So even if you use Redux on page components, you should use pure() or shouldUpdate() in the deeper part of the rendering tree.
Also, beware of Redux comparing props with strict mode. Because Redux binds state to the props of the component, if you modify an object on state, the props of Redux will miss it. This is why you must use the immutability principle in reducer
For example, in admin-on-REST, click the header to dispatch a SET_SORT action. The Reducer listening to this action must replace the objects in the state, not update them.
// in listReducer.js export const SORT_ASC = 'ASC'; export const SORT_DESC = 'DESC'; const initialState = { sort: 'id', order: SORT_DESC, page: 1, perPage: 25, filter: {}, }; export default (previousState = initialState, { type, payload }) => { switch (type) { case SET_SORT: if (payload === previousState.sort) { // inverse sort order return { ... previousState, order: oppositeOrder(previousState.order), page: 1, }; } // replace sort field return { ... previousState, sort: payload, order: SORT_ASC, page: 1, }; / /... default: return previousState; }};
Copy the code
Again, when Redux checks for changes with ‘===’, it finds the difference in the state object and redraws the Datagrid. But if we change state, Redux will ignore the state change and mistakenly skip the redraw:
// don't do this at home export default (previousState = initialState, { type, payload }) => { switch (type) { case SET_SORT: if (payload === previousState.sort) { // never do this previousState.order = oppositeOrder(previousState.order); return previousState; } // never do that either previousState.sort = payload; previousState.order = SORT_ASC; previousState.page = 1; return previousState; / /... default: return previousState; }};
Copy the code
To be immutable, other developers like to use Immutable mutable js, also from Facebook. I don’t think this is necessary because ES6 destructuring assignment makes it easy to selectively replace component properties. Immutable is also cumbersome (60kB), so think twice before adding it to your project.
To choose
To prevent useless drawing of connected components (in Redux), you must ensure that the mapStateToProps method does not return a new object each time it is called.
Take the
component in admin-on-REST as an example. It gets a list of records (e.g., posts, comments, etc.) for the current resource from state with the following code:
// in List.js import React from 'react'; import { connect } from 'react-redux'; const List = (props) => ... const mapStateToProps = (state, props) => { const resourceState = state.admin[props.resource]; return { ids: resourceState.list.ids, data: Object.keys(resourceState.data) .filter(id => resourceState.list.ids.includes(id)) .map(id => resourceState.data[id]) .reduce((data, record) => { data[record.id] = record; return data; }, {})}; }; export default connect(mapStateToProps)(List);
Copy the code
State contains an array of previously fetched records indexed by Resource. For example, state.admin.posts.data contains a series of posts:
{ 23: { id: 23, title: "Hello, World", /* ... */ }, 45: { id: 45, title: "Lorem Ipsum", /* ... */ }, 67: { id: 67, title: "Sic dolor amet", /* ... * /}},
Copy the code
The mapStateToProps method filters the state object and returns only the parts shown in the list. As follows:
{ 23: { id: 23, title: "Hello, World", /* ... */ }, 67: { id: 67, title: "Sic dolor amet", /* ... * /}},
Copy the code
The problem is that each time mapStateToProps executes, it returns a new object, even if the underlying object has not been changed. As a result, the
component is redrawn every time, even if only part of the state changes – date or IDS changes cause the ID to change.
Reselect addresses this issue through the memo model. Instead of evaluating props directly in mapStateToProps, using selector from resELECT returns the same output if the input has not changed.
import React from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect'
const List = (props) => ...
const idsSelector = (state, props) => state.admin[props.resource].ids
const dataSelector = (state, props) => state.admin[props.resource].data
const filteredDataSelector = createSelector(
idsSelector,
dataSelector
(ids, data) => Object.keys(data)
.filter(id => ids.includes(id))
.map(id => data[id])
.reduce((data, record) => {
data[record.id] = record;
return data;
}, {})
)
const mapStateToProps = (state, props) => {
const resourceState = state.admin[props.resource];
return {
ids: idsSelector(state, props),
data: filteredDataSelector(state, props),
};
};
export default connect(mapStateToProps)(List);
Copy the code
The
component is now redrawn only when a subset of state changes.
As a recombination problem, reselect Selector is a pure function that is easy to test and combine. It is the best way to write a selector for a Redux Connected component.
As your components become more “pure,” you start detecting bad patterns that lead to useless redraws. The most common is the use of object literals in JSX, which I prefer to call the “infamous {{“. Allow me to illustrate:
import React from 'react';
import MyTableComponent from './MyTableComponent';
const Datagrid = (props) => (
<MyTableComponent style={{ marginTop: 10 }}>
...
</MyTableComponent>
)
Copy the code
Each time the
component is redrawn, the
component gets a new value for its style property. So even if
is pure, it will be redrawn every time the
is redrawn. In fact, every time you pass an object literal to a child component as a property value, you break a pure function. The solution is simple:
import React from 'react';
import MyTableComponent from './MyTableComponent';
const tableStyle = { marginTop: 10 };
const Datagrid = (props) => (
<MyTableComponent style={tableStyle}>
...
</MyTableComponent>
)
Copy the code
This seems basic, but I’ve seen this mistake so many times that I’ve developed a keen intuition for detecting the infamous {{. Let me replace them all with constants.
Another suspect commonly used to hijack pure functions is react.cloneElement (). If you pass the prop value as the second parameter to the method, each render generates a new Clone component with new props.
// bad
const MyComponent = (props) => <div>{React.cloneElement(Foo, { bar: 1 })}</div>;
// good
const additionalProps = { bar: 1 };
const MyComponent = (props) => <div>{React.cloneElement(Foo, additionalProps)}</div>;
Copy the code
The Material – UI has been bothering me for a while, for example:
import { CardActions } from 'material-ui/Card'; import { CreateButton, RefreshButton } from 'admin-on-rest'; const Toolbar = ({ basePath, refresh }) => ( <CardActions> <CreateButton basePath={basePath} /> <RefreshButton refresh={refresh} /> </CardActions> ); export default Toolbar;
Copy the code
Although
is a pure function, it is drawn every time the Toolbar draws. That’s because the
of the material- UI adds a special style in order to make the first child node fit margin – it uses an object literal to do this. So
receives a different style attribute each time. I solved this problem with Recompose’s onlyUpdateForKeys() HOC.
// in Toolbar.js import onlyUpdateForKeys from 'recompose/onlyUpdateForKeys'; const Toolbar = ({ basePath, refresh }) => ( ... ) ; export default onlyUpdateForKeys(['basePath', 'refresh'])(Toolbar);
Copy the code
conclusion
There are many other ways to make React applications faster (use keys, lazy load reroute, react-Addons-perf package, use ServiceWorkers to cache application state, use isomorphism, and so on), But shouldComponentUpdate is the first step – and most useful – to implement correctly.
React is not fast by default, but it offers a number of tools to speed up applications of any size. This may be counterintuitive, especially since many frameworks offer alternatives to React that claim to be n times faster. React puts developer experience ahead of performance. That’s why developing large applications with React is such a pleasant experience, with no surprises and constant implementation speed.
Just remember to profile your application every once in a while and make time to add pure() calls where necessary. Don’t optimize at first, and don’t spend too much time over-optimizing each component — unless you’re on mobile. Remember to test on different devices to give users a good impression of how responsive your application is.
The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. Android, iOS, React, front end, back end, product, design, etc. Keep an eye on the Nuggets Translation project for more quality translations.