• 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.

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.

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.

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.