📜 ⬆️ ⬇️

React slow, React fast: React application optimization in practice

Hello! I want to share my translation of the article React is Slow, React is Fast: Optimizing Reactor Apps in Practice by François Zaninotto . I hope this will be useful to someone.


Summary:


  1. React Performance Measurement
  2. Why are you updated?
  3. Optimization through component splitting
  4. shouldComponentUpdate
  5. Recompose
  6. Redux
  7. Reselect
  8. Beware of object literals in JSX
  9. Conclusion

React can be slow. I want to say that any React medium sized application can be slow. But before looking for a replacement for it, you should know that any average application on Angular or Ember can also be slow.


The good news is that if you really care about performance, then making a React app very fast is pretty easy . About this - further in the article.


React Performance Measurement


What do I mean by slow? Let me give you an example:


I am working on one open-source project called admin-on-rest , using material-ui and Redux to provide a graphical user interface (GUI) admin panel for any API. In this application there is a page that displays a list of records in a table. When the user changes the sort order, or goes to the next page, or filters the output, the interface is not as responsive as we would like.


The following animated screencast, slowed down 5 times, shows how the update occurs:


An animated screencast, slowed down 5 times, shows how the update occurs.

To understand what is happening, I add ?react_perf at the end of the URL. This activates component profiling capability, which is available from React 15.4. First, I'm waiting for the initial loading of the data table. Next, I open the Timeline tab in the Chrome developer tools, click on the "Record" button and click on the table title on the page to update the sorting order.


After updating the data, I click on the record button again to stop it. Chrome will display a yellow graph below the "User Timing" label.


Chrome displays a yellow graph under the label User Timing

If you have never seen this graph, it may seem daunting, but, in fact, it is very easy to use. This graph shows the running time of each of your components. It does not show the time of the internal React components (you still cannot optimize them), so it allows you to focus on optimizing your own code.


The timeline displays the stages of recording the operation of the application and allows you to bring closer the moment when I clicked on the table header:


The timeline displays the stages of recording the operation of the application.
')

It seems that my application redraws the <List> component immediately after clicking on the sort button, and before retrieving data through REST. It takes more than 500 ms. The application simply updates the sorting icon in the table header and displays a gray screen indicating the data is loaded.


In other words, the application takes 500 ms to visually display the response to the click. Half a second is a significant figure - UI experts say that users consider an application's response to be instant only when it is less than 100 ms . Application response over 100 ms is what I call “slow”.


Why are you updated?


On the graph above you can see a lot of tiny holes. This is a bad sign, as it means that many components are redrawn. The graph shows that the <Datagrid> update takes the most time. Why did the application update the entire data table before it received new data? Let's figure it out.


Attempts to understand the reasons for redrawing often involve adding console.log() to the render() function. For functional components, you can use the following higher order component (HOC):


 // src/log.js const log = BaseComponent => props => { console.log(`Rendering ${BaseComponent.name}`); return <BaseComponent {…props} />; } export default log; // src/MyComponent.js import log from './log'; export default log(MyComponent); 

Tip: It is also worth noting why-did-you-update is another tool for the effectiveness of React. This npm package causes React to display warnings to the console whenever the component is redrawn with the same props. I warn you: the output in the console is quite detailed and it does not work with functional components.

In the example, when the user clicks on the column header, the application performs an action that changes the state: the list sorting order ( currentSort ) is updated. This state change triggers a redrawing of the <List> page, which in turn redraws the entire <Datagrid> component. We want the table header to immediately draw the change of the sorting icon as a response to user actions.


Usually, React becomes slow not because of one slow component (which will be displayed on the graph as one big hole). In most cases, React becomes slow due to the useless redrawing of many components.


You may have read that VirtualDOM in React is very fast. This is true, but in a medium-sized application, a full redraw can easily contain hundreds of components. Even the fastest VirtualDOM template engine cannot do it in less than 16 ms.


Optimization through component splitting


Here is the render() method of the <Datagrid> component:


 // Datagrid.js render() { const { resource, children, ids, data, currentSort } = this.props; return ( <table> <thead> <tr> {Children.map(children, (field, index) => <DatagridHeaderCell key={index} field={field} currentSort={currentSort} updateSort={this.updateSort} /> )} </tr> </thead> <tbody> {ids.map(id => ( <tr key={id}> {Children.map(children, (field, index) => <DatagridCell record={data[id]} key={`${id}-${index}`} field={field} resource={resource} /> )} </tr> ))} </tbody> </table> ); } 

It seems that this is a very simple implementation of tabular data, but it is extremely inefficient. Each <DatagridCell> causes a draw of at least two or three components. As you can see on the animated screencast of the interface at the beginning of the article, the list contains 7 columns and 11 rows, and this means that 7 * 11 * 3 = 231 components are being redrawn. And all this is a waste of time, since only currentSort subject to currentSort . Although React does not update the real DOM (provided that VirtualDOM has not changed), it still takes about 500 ms to process all components.


To avoid the useless redrawing of the table body, first I must * extract * it:


 // 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> ); ); } 

I created a new <DatagridBody> component by extracting logic from the table body:


 // DatagridBody.js import React, { Children } from 'react'; const DatagridBody = ({ resource, ids, data, children }) => ( <tbody> {ids.map(id => ( <tr key={id}> {Children.map(children, (field, index) => <DatagridCell record={data[id]} key={`${id}-${index}`} field={field} resource={resource} /> )} </tr> ))} </tbody> ); export default DatagridBody; 

In itself, the extraction of the body of the table does not affect the performance, but it opens up opportunities for optimization. Large general purpose components are difficult to optimize. It’s easier to handle small components that are responsible only for one thing.


shouldComponentUpdate


The React documentation describes very clearly the way to avoid useless redrawing by using shouldComponentUpdate() . By default, React always displays the component in VirtualDOM. In other words, your job as a developer is to check whether the component props have changed, and if not, then skip its redrawing.


In the case of the <DatagridBody> component, there should be no redrawing in it until the props changes.


Therefore, the component should look like this:


 import React, { Children, 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}> {Children.map(children, (field, index) => <DatagridCell record={data[id]} key={`${id}-${index}`} field={field} resource={resource} /> )} </tr> ))} </tbody> ); } } export default DatagridBody; 

Tip: Instead of manually setting shouldComponentUpdate () , I could inherit this class from PureComponent instead of Component . PureComponent will compare all props using a strict comparison ( === ) and redraw only if the props have changed. But I know that resource and children cannot change in this context, so I don’t need to compare them.

Due to this optimization, redrawing <Datagrid> after clicking on the table header skips its contents and all 231 components. This reduced the update time from 500 ms to 60 ms. This is a net performance increase of more than 400 ms!


After optimization

Tip: Do not be deceived by the width of the graph, it is even closer than on the previous graph. It is definitely better!

The method shouldComponentUpdate removed a lot of holes in the graph and reduced the total drawing time. I can use the same method to avoid even large redraws (for example, do not redraw the sidebar, action buttons, unchanged table headers, pagination). After about an hour of messing around with all this, the entire page is rendered in just 100 ms after clicking on the column header. This is fast enough - even if there is still something left to optimize.


Adding a method shouldComponentUpdate may seem cumbersome, but if you care about performance, most components should contain it.


But do not insert it wherever you can - doing shouldComponentUpdate in fairly simple components can sometimes slow down its rendering. Do not do this too early in the application life cycle. Add this method only as your application grows, when you can identify performance problems in your components.


Recompose


I am not particularly happy with the previous changes in <DatagridBody> : because of shouldComponentUpdate I had to transform a simple, functional component into a class. This adds many lines of code, each of which has its price - in the form of writing, debugging and support.


Fortunately, you can implement the logic of shouldComponentUpdate in a higher order component (HOC), thanks to recompose . This is a functional tool for React that provides, for example, the pure() HOC function:


 // DatagridBody.js import React, { Children } from 'react'; import pure from 'recompose/pure'; const DatagridBody = ({ resource, ids, data, children }) => ( <tbody> {ids.map(id => ( <tr key={id}> {Children.map(children, (field, index) => <DatagridCell record={data[id]} key={`${id}-${index}`} field={field} resource={resource} /> )} </tr> ))} </tbody> ); export default pure(DatagridBody); 

The only difference between this code and the initial implementation is in the last line: I export pure(DatagridBody) instead of DatagridBody . pure is similar to PureComponent , but without an extra boilerplate.


I can even be more specific and focus only on those props, about which I know for sure that they can change, using shouldUpdate() instead of pure() :


 // DatagridBody.js import React, { Children } from 'react'; import shouldUpdate from 'recompose/shouldUpdate'; const DatagridBody = ({ resource, ids, data, children }) => ( ... ); const checkPropsChange = (props, nextProps) => (nextProps.ids !== props.ids || nextProps.data !== props.data); export default shouldUpdate(checkPropsChange)(DatagridBody); 

checkPropsChange is a pure function, and I can even export it for unit testing.


The recompose library offers more efficient HOCs, such as onlyUpdateForKeys() , which performs the same check that I did in my checkPropsChange :


 // DatagridBody.js import React, { Children } from 'react'; import onlyUpdateForKeys from 'recompose/onlyUpdateForKeys'; const DatagridBody = ({ resource, ids, data, children }) => ( ... ); export default onlyUpdateForKeys(['ids', 'data'])(DatagridBody); 

I warmly recommend recompose . In addition to optimizing performance, it helps you extract the logic of data sampling, compose HOC and work with props in a functional and tested style.


Redux


If you use Redux to manage the state of the application ( which I also recommend ), then the components connected to it are already clean. No need for any other HOC.


Just remember, if only one property has changed, then the connected component will be redrawn - and all its descendants too. Therefore, if you are using Redux for page components, you should use pure() or shouldUpdate() for the underlying components.


But also remember that Redux uses strict comparison for props. Since Redux binds the state with the props component, if you change the object in state, Redux will simply ignore this. And for this reason you must use immiability in your reducers.


For example, in admin-on-rest, click on the heading of the table dispatch SET_SORT action. Reducer, which listens to this action, should replace the object in the state, and not update it:


 // 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) { //    return { ...previousState, order: oppositeOrder(previousState.order), page: 1, }; } //   sort return { ...previousState, sort: payload, order: SORT_ASC, page: 1, }; // ... default: return previousState; } }; 

Following the code of this reducer, when Redux checks the state for changes using a triple comparison, it detects that the state object has changed and redraws the table with the data. But if we mutated the state, Redux would have missed this change and accordingly would not redraw anything:


 //       export default (previousState = initialState, { type, payload }) => { switch (type) { case SET_SORT: if (payload === previousState.sort) { //     previousState.order= oppositeOrder(previousState.order); return previousState; } //      previousState.sort = payload; previousState.order = SORT_ASC; previousState.page = 1; return previousState; // ... default: return previousState; } }; 

To write an immutable reducers, some developers use the immutable.js library, which is also from Facebook. But since ES6 has simplified the selective replacement in the component properties, I do not think that this library is necessary. In addition, it is heavy (60 kB), so think twice before adding it depending on your project.


Reselect


To prevent unnecessary rendering of components connected to Redux, you must also ensure that the mapStateToProps function mapStateToProps not return new objects each time it is called.


Take, for example, the <List> component in admin-on-rest. It takes from the state a list of entries for the current resource (for example, posts, comments, etc.) with the following code:


 // 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); 

State contains an array of all previously loaded records indexed by the resource. For example, state.admin.posts.data contains a list of posts:


 { 23: { id: 23, title: “Hello, World”, /* … */ }, 45: { id: 45, title: “Lorem Ipsum”, /* … */ }, 67: { id: 67, title: “Sic dolor amet”, /* … */ }, } 

The mapStateToProps function filters the state object and returns only those records that are actually displayed in the list. Something like that:


 { 23: { id: 23, title: “Hello, World”, /* … */ }, 67: { id: 67, title: “Sic dolor amet”, /* … */ },\ } 

The problem is that each time the mapStateToProps function is mapStateToProps , it returns a new object, even if the internal objects have not changed. As a result, the <List> component is redrawn every time when something changes in a state — while, as the date or id changes, only the id should change.


Reselect solves this problem through memoization. Instead of calculating props directly in mapStateToProps , you use selector from reselect, which returns the same object if no changes were made to it.


 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); 

Now the <List> component will be redrawn only when the state subset changes.


As for recompose, selectors are pure functions, easy to test and build. Writing your selector for components connected to Redux is a good practice.


Beware of object literals in JSX


One day your component will become even more “clean”, and you may find bad patterns in your code that lead to useless redrawing. The most common example of this is the use of object literals in JSX, which I like to call "The infamous {{ ". Let me give you an example:


 import React from 'react'; import MyTableComponent from './MyTableComponent'; const Datagrid = (props) => ( <MyTableComponent style={{ marginTop: 10 }}> ... </MyTableComponent> ) 

The style property of the <MyTableComponent> component gets a new value each time the <Datagrid> component is drawn. Thus, even if <MyTableComponent> clean, it will still be redrawn when redrawing <Datagrid> . In fact, every time you pass an object literal as a property to a child component, you violate purity. The solution is simple:


 import React from 'react'; import MyTableComponent from './MyTableComponent'; const tableStyle = { marginTop: 10 }; const Datagrid = (props) => ( <MyTableComponent style={tableStyle}> ... </MyTableComponent> ) 

It looks quite simple, but I have seen this error so many times that I developed a sense of finding the "infamous {{ " in JSX. I regularly replace them with constants.


The next suspect to steal the purity of a component is React.CloneElement() . If you pass the property as the value of the second parameter, the cloned element will receive new props with each drawing.


 //  const MyComponent = (props) => <div>{React.cloneElement(Foo, { bar: 1 })}</div>; //  const additionalProps = { bar: 1 }; const MyComponent = (props) => <div>{React.cloneElement(Foo, additionalProps)}</div>; 

I burned it a couple of times with material-ui using the following code:


 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; 

Although the <CreateButton> component is clean, it is drawn every time the <Toolbar> drawn. This is all because the <CardAction> component from material-ui adds a special style to the first descendant to be placed in the margins - and he does this with the help of the object literal. Therefore, <CreateButton> gets a different style object every time. I was able to solve this using the HOC function onlyUpdateForKeys() from recompose.


 // Toolbar.js import onlyUpdateForKeys from 'recompose/onlyUpdateForKeys'; const Toolbar = ({ basePath, refresh }) => ( ... ); export default onlyUpdateForKeys(['basePath', 'refresh'])(Toolbar); 

Conclusion


There are many more things that need to be done to keep an application on React fast (use keys, lazy loading of heavy routes, react-addons-perf , ServiceWorkers package for caching application state, add isomorphism, etc.), but the correct implementation shouldComponentUpdate is the first and most effective step.


By itself, React is not fast, but it offers all the tools to make an application of any size fast.


This seems illogical, especially when many frameworks offer alternatives to React, arguing that they are N times faster than it. But React focuses on the convenience and experience of the developer, not productivity. This is the reason why developing large applications with React is a pleasant experience, without bad surprises and with a steady implementation rate.


Remember to profile your application from time to time and devote some time to adding pure() calls if necessary. But do not do it at the very beginning, and do not spend too much time optimizing each component - except if you don’t do it for mobile devices. And do not forget to test on different devices in order to get a good impression of the responsiveness of your application from the user's point of view.


If you want to learn more about React performance optimization, here is a list of excellent articles on this topic:


Source: https://habr.com/ru/post/327364/


All Articles