Top 5 Practices to Boost React Performance

May 29, 2018 0 Comments

Top 5 Practices to Boost React Performance

 

 

React has been around for three years, and it has grown to be one of the most popular libraries for building front-end views. The JavaScript community undeniably loves it. Developers voted for React in the StackOverflow survey this year, and React ended up being the second most loved library on the planet.

I personally love React. I think that React is great because of its declarative, top-down approach and its simplicity compared to other JavaScript frameworks and libraries. However, there are certain places where you might not get the best out of React out of the box. Performance is one such place where React can do a lot better, and that’s what we’re going to focus in this post.

If you’ve never been to this part of React before, this post should help you understand your options. In this article, I’ll cover the top five ways that you can boost React’s performance. But before we get started, we need to understand how React works and how it updates the DOM.

React Basics — So, what happens under the hood?

Let’s start with the DOM. DOM, as you might already know, constructs a tree of objects and nodes. HTML elements become nodes in the DOM. In layman’s terms, it is a standard that defines how to get, change, add, or delete HTML elements. However, DOM wasn’t created with dynamic UI in mind. When the DOM needs to be updated, it has to go through a Reflow/Layout stage and the DOM nodes need to be repainted, which is slow. When you have an application with thousands of <div>s, there is a severe performance issue.
PhoneGapOptPerf1.png

React uses a different strategy to update your DOM. The process is popularly known as Reconciliation. When your browser initially renders your application, React builds a tree of elements. Let’s imagine that this how React builds its tree:

React-Tutorial.jpg

But what when a component’s state changes, the render() method returns a different tree of React elements. React has to calculate the changes it has to make to the DOM, and it does so by creating a virtual DOM and then comparing that to the real DOM.

The good thing about virtual DOM is that it updates only the part of the DOM that needs to be updated. React uses a diffing algorithm that tries to minimize the number of operations to transform the tree. However, the diffing algorithm isn’t perfect. Let’s say that values of the nodes in yellow has changed, and needs to be updated.

React-Components-Dirty.jpg

However, the diffing algorithm isn’t perfect. If the updated props were passed in from the com React ends up updating entire subtree like this.

React-VirtualDOM-Best-Practices.jpg

As you can see, React is unnecessarily rendering and diffing component subtrees. Instead, there are ways that you can re-render only the nodes that are directly affected by the props change and not the entire subtree.

React-Component-Reconciliation.jpg

When React wastes resources and CPU on rendering components that haven’t changed, we call it wasted renders. Wasted renders can throttle performance — but if we could streamline the diffing process further, we might be able to boost the speed significantly. The rest of the post will cover how to boost React performance.

Before you jump to any conclusions, you should consider benchmarking the application to see how well the app performs. React used to have a tool called react-addon-perf that was recently retired and it isn’t available in the latest version of React. Instead, React recommends using the browser’s profiling tools to get the same result.

I personally prefer Chrome’s developer tools because there are tons of features under the Performance tab that let you profile and debug your application with ease. To create a performance profile for your application, follow the steps below:

  1. Make sure that you’ve exported the JS with source-maps or that you’re in development mode.
  2. Temporarily disable all React extensions, like React DevTools, because they can impact the results.
  3. Set up the profiling tool. Open the Chrome developer tools and go to the performance tab. You can slow down the JavaScript execution so that the performance issues become more apparent.
  4. Now, navigate to the page that you would like to profile and then press the red button that says “Record.” If you would like to create a performance trace of the initial render, you should choose the start profiling and reload button. Now, perform the desired action and stop the recording.
  5. Chrome will return visualization of the performance data in vertical bands. Zoom in onto the User Timing band to see each component’s performance.
  6. You can select individual components and the bottom up tab will give you insights into activities that take up most of the time.

For a more detailed overview, I’d recommend Ben Schwarz’s article on Debugging React.

There’s also a library, known as why-did-you-update, that is an excellent tool for component analysis. It gives you a visualization of the props before and after and notifies you about components that shouldn’t have been rerendered.To install the tool, run

npm install --save why-did-you-update

Add this code into your index.js file.

import React from 'react'; if (process.env.NODE_ENV !== 'production') { const {whyDidYouUpdate} = require('why-did-you-update'); whyDidYouUpdate(React);
}

I just tested it on one of the contact list applications that I created earlier, and the results were definitely interesting.

Screen Shot 2018-05-22 at 5.20.40 PM.png

Prevent Unnecessary Rendering

React re-renders a component when its props or state gets updated. This is the default behavior, and, since React updates only the relevant DOM nodes, this process is usually fast. However, there are certain cases where the component’s output stays the same regardless of whether the props have changed.

To alter the default implementation, you can use a lifecycle hook called shouldComponentUpdate(). This lifecycle hook doesn’t get invoked during the initial render, but only on subsequent re-renders. shouldComponentUpdate() is triggered when the props have changed and it returns true by default.

shouldComponentUpdate(nextProps, nextState) { return true;
}

If you’re sure that your component doesn’t need to re-render regardless of whether the props have updated or not, you can return false to skip the rendering process.

class ListItem extends Component { shouldComponentUpdate(nextProps, nextState) { return false } render() { // Let the new props flow, I am not rendering again. }
}

Alternatively, if you need to update only when certain props are updated, you can do something like this:

class ListItem extends Component { shouldComponentUpdate(nextProps, nextState) { return nextProps.isFavourite != this.props.isFavourite; } ...
}

Here, we’re checking whether the current value of isFavourite has changed before each render (nextProps holds the new value of the prop) and if yes, the expression returns true. The component gets re-rendered. The changes in any other props won’t trigger a render() since we’re not comparing them in shouldComponentUpdate().

What about React.PureComponent?

React API includes a performant version of Component known as PureComponent. You might have already heard about it. However, the performance gains come with a few string attached.

A React.PureComponent is pretty much like a normal Component but the key difference being, it handles the shouldComponentUpdate() method for you. When the props or the state update, PureComponent will do a shallow comparison on them and update the state.

So, what is shallow comparison? Here’s excerpt from an answer on StackOverflow:

Shallow compare does check for equality. When comparing scalar values (numbers, strings) it compares their values. When comparing objects, it does not compare their's attributes - only their references are compared (e.g. "do they point to same object?).

What if the props passed by the parent component, say an object, has changed it’s reference. This is known as mutation and this is bad. If you end up changing references, you’re actually mutating your props/state and your pure child component won’t update.

In my opinion, you should use PureComponent only if you have a clear understanding of how immutable data works and when to use forceUpdate() to update your component.

Debounce Input Handlers

This concept is not specific to React, or any other front-end library. Debouncing has long been used in JavaScript to successfully run expensive tasks without hindering performance. A debounce function can be used to delay certain events so that it doesn’t get fired up every millisecond. This helps you limit the number of API calls, DOM updates, and time consuming tasks. For instance, when you’re typing something into the search menu, there is a short delay before all of the suggestions pop up. The debounce function limits the calls to onChange event handler. In many cases, the API request is usually made after the user has stopped typing.

Let me focus more on the debouncing strategy for input handlers and DOM updates. Input handlers are a potential cause of performance issues. Let me explain how:

export default class SearchBar extends React.Component { constructor(props) { super(props); this.onChange = this.onChange.bind(this); } onChange(e) { this.props.onChange(e.target.value); } render () { return ( <label> Search </label> <input onChange={this.onChange}/> ); }
}

On every input change, we’re calling this.props.onChange(). What if there is a long-running sequence of actions inside that callback? That’s where debounce comes it handy. You can do something like this:

import {debounce} from 'throttle-debounce'; export default class SearchBarWithDebounce extends React.Component { constructor(props) { super(props); this.onChange = this.onChange.bind(this); this.onChangeDebounce = debounce( 300, value => this.props.onChange(value) ); } onChange(e) { this.onChangeDebounce(e.target.value); } render () { return ( <input onChange={this.onChange}/> ); }
}

The DOM update happens after the user has finished typing, which is exactly what we need. If you’re curious about debouncing AJAX calls, you can do something like this:

import {debounce} from 'throttle-debounce'; export default class Comp extends Component { constructor(props) { super(props); this.callAPI = debounce(500, this.callAPI); } onChange(e) { this.callAPI(e.target.value); } callAjax(value) { console.log('value :: ', value); // AJAX call here } render() { return ( <div> <input type="text" onKeyUp={this.onChange.bind(this)}/> </div> ); }
}

Optimize React for Production

A minified production build is faster and more optimized for heavy workloads, whereas a development build is meant to highlight errors and warnings in the console. Although these warnings help the developer debug errors, they can directly impact the performance of the application.

You can use the Network tab in your Developer Tools to see what is actually slowing you down. React bundle size is usually the culprit, and you need to minify it before you push it for production. If you bootstrapped the application using create-react-app, you can get a production build by running npm run build.

When you run this, create-react-app will minify the code, obfuscate/uglify it (that happens as a result of minification) and generate source maps. The end user is able to load the scripts faster than the version that was not minified. The source maps generated are particularly handy if you decide to navigate around the code through the Developer console. The source maps are loaded on demand and you can delete it before actual deployment.

If you’re using webpack, you can build a production environment by running webpack -p. React also offers production-ready single files for React and ReactDOM.

<script src="https://unpkg.com/react@16/umd/react.production.min.js">
</script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js">
</script>

There are ways to reduce the bundle size even further, like using Preact, but you need to take that path only if the initial load time is noticeably slow.

Optimizing your application’s resource is another thing that you need to consider. This has nothing to do with React, but can indeed speed up your application. Resources such as images, icons and other media files can slow down the application if they are rendered uncompressed. The Network tab in the Developer Tools should give you insights into that too. If you’re using static resources, optimize and compress them before pushing them into the server. Use a CDN if your server and the potential users are not in close proximity. Here is a nice article written by David Lazic that covers optimizing the production build further.

Micro-optimizations To Boost React Performance

Micro-optimizations are certain details that won’t have an immediate impact on your application, but will help you in the long run. There are many-micro optimizations around that you should know about. Some of these practices also help you improve the readability and maintainability of your codebase.

Functional components vs. Class components

You might already know that most React developers prefer to split their components based on the logic that goes into those components — functional components and class components. Functional components are easier to write and read, and they are particularly useful for building the UI or the presentational layer. Class components, on the other hand, are ideal for building containers. The containers make API calls, dispatch actions to the store, and use the state to hold data. They then pass the data as props to the presentational components.

Until React v16 was released, both the functional components and the class components had the same code path. Although the functional components appeared to have fewer lines of code, they were neither optimized nor offered speed improvements because they were converted to Class components under the hood. However, the scenario has changed with the release of V6.

Here is an excerpt from the official docs:

Functional components in React 16 don't go through the same code path as class components, unlike in previous versions, where they were converted to classes and would have the same code path. Class components have additional checks required and overhead in creating the instances that simple functions don't have.

Bind Function Early

Binding functions in the render() method is a bad idea because React will create a new function on each render.

class Button extends React.Component { handleClick() { console.log('Yay!'); } render() { return ( <button onClick={this.handleClick.bind(this)}/> ); }
}

Arrow functions inside the render method is bad too, unfortunately.

class Button extends React.Component { handleClick() { console.log('Yay'); } render() { return ( <button onClick={() => this.handleClick()}/> ); }
}

The process of creating a new function is usually fast and won’t slow you down, to be honest. But if you’ve implemented shouldComponentUpdate() or if you’re using PureComponent base class, React sees new props on each render and the child components are re-rendered unnecessarily.

To fix this issue, you can bind functions early like this:

class Button extends React.Component { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); } handleClick() { console.log('Yay!'); } render() { return ( <button onClick={this.handleClick}/> ); }
}

Alternatively you can use something like class instance fields. However, it’s an experimental feature that has not yet made it to the standards. But you can use it in your project with the help of Babel.

class Button extends React.Component { // This is a class field handleClick = () => { console.log('Yay!'); } render() { return ( <button onClick={this.handleClick}/> ); }
}

Final Words

In this post, I’ve covered some of the popular and clever ways to boost React’s performance. On a general note, you don’t have to be concerned until you start seeing actual bottlenecks. I prefer to go with the “Don’t optimize unless you need to” approach. That’s why I’d recommend benchmarking components first and measuring whether they are actually slowing you down. Your browser’s developer tools will also give insights into other non-React factors that could be affecting your app. Use that as a starting point and that should help you get started.

If you have anything to share, let us know in the comments.


Tag cloud