DEV Community

Codementor for Codementor

Posted on • Updated on

10 Performance Optimization Techniques for React Apps

Internally, React uses several clever techniques to minimize the number of costly DOM operations required to update the UI. While this will lead to a faster user interface without specifically optimizing for performance for many cases, there are ways where you can still speed up your React application. This post will go over some useful techniques you can use to improve your React code.

1. Using Immutable Data Structures

Data immutability is not an architecture or design pattern, it’s an opinionated way of writing code. This forces you to think about how you structure your application data flow. In my opinion, data immutability is a practice that revolves around a strict unidirectional data flow.

Data immutability, which comes from the functional programming world, can be applied to the design of front-end apps. It can have many benefits, such as:

  • Zero side-effects;
  • Immutable data objects are simpler to create, test, and use;
  • Helps prevent temporal coupling;
  • Easier to track changes.

In the React landscape, we use the notion of Component to maintain the internal state of components, and changes to the state can cause the component to re-render.

React builds and maintains an internal representation of the rendered UI (Virtual DOM). When a component’s props or state changes, React compares the newly returned element with the previously rendered one. When the two are not equal, React will update the DOM. Therefore, we have to be careful when changing the state.

Let’s consider a User List Component:

state = {
       users: []
   }

   addNewUser = () =>{
       /**
        *  OfCourse not correct way to insert
        *  new user in user list
        */
       const users = this.state.users;
       users.push({
           userName: "robin",
           email: "email@email.com"
       });
       this.setState({users: users});
   }
Enter fullscreen mode Exit fullscreen mode

The concern here is that we are pushing new users onto the variable users, which is a reference to this.state.users.

Pro Tip: React state should be treated as immutable. We should never mutate this.state directly, as calling setState() afterward may replace the mutation you made.

So what’s wrong with mutating state directly? Let’s say we overwrite shouldComponentUpdate and are checking nextState against this.state to make sure that we only re-render components when changes happen in the state.

 shouldComponentUpdate(nextProps, nextState) {
    if (this.state.users !== nextState.users) {
      return true;
    }
    return false;
  }
Enter fullscreen mode Exit fullscreen mode

Even if changes happen in the user's array, React won’t re-render the UI as it’s the same reference.

The easiest way to avoid this kind of problem is to avoid mutating props or state. So the
addNewUser method could be rewritten using concat:

   addNewUser = () => {
       this.setState(state => ({
         users: state.users.concat({
           timeStamp: new Date(),
           userName: "robin",
           email: "email@email.com"
         })
       }));
   };
Enter fullscreen mode Exit fullscreen mode

For handling changes to state or props in React components, we can consider the following immutable approaches:

  • For arrays: use [].concat or es6 [ ...params]
  • For objects: use Object.assign({}, ...) or es6 {...params}

These two methods go a long way when introducing immutability to your code base.

But it’s better to use an optimized library which provides a set of immutable data structures. Here are some of the libraries you can use:

  • Immutability Helper: This is a good library when it’s comes to mutating a data copy without changing the source.
  • Immutable.js: This is my favorite library as it provides a lot of persistent immutable data structures, including: List, Stack, Map, OrderedMap, Set, OrderedSet, and Record.
  • Seamless-immutable: A library for immutable JavaScript data structures that are backward-compatible with normal arrays and objects.
  • React-copy-write: An immutable React state management library with a simple mutable API, memoized selectors, and structural sharing.

Pro Tip: React setState method is asynchronous. This means that rather than immediately mutating this.state, setState() creates a pending state transition. If you access this.state after calling this method, it would potentially return the existing value. To prevent this, use the callback function of setState to run code after the call is completed.

Additional Resources:

The original post, 21 Performance Optimization Techniques for React Apps, is published on the Codementor Blog

2. Function/Stateless Components and React.PureComponent

In React, function components and PureComponent provide two different ways of optimizing React apps at the component level.

Function components prevent constructing class instances while reducing the overall bundle size as it minifies better than classes.

On the other hand, in order to optimize UI updates, we can consider converting function components to a PureComponent class (or a class with a custom shouldComponentUpdate method). However, if the component doesn’t use state and other life cycle methods, the initial render time is a bit more complicated when compared to function components with potentially faster updates.

When should we use React.PureComponent?

React.PureComponent does a shallow comparison on state change. This means it compares values when looking at primitive data types, and compares references for objects. Due to this, we must make sure two criteria are met when using React.PureComponent:

  • Component State/Props is an immutable object;
  • State/Props should not have a multi-level nested object.

Pro Tip: All child components of React.PureComponent should also be a Pure or functional component.

3. Multiple Chunk Files

Your application always begins with a few components. You start adding new features and dependencies, and before you know it, you end up with a huge production file.

You can consider having two separate files by separating your vendor, or third-party library code from your application code by taking advantage of CommonsChunkPlugin for webpack. You’ll end up with vendor.bundle.js and app.bundle.js. By splitting your files, your browser caches less frequently and parallel downloads resources to reduce load time wait.

Note: If you are using the latest version of webpack, you can also consider SplitChunksPlugin

4.Using Production Mode Flag in Webpack

If you are using webpack 4 as a module bundler for your app, you can consider setting the mode option to production. This basically tells webpack to use the built-in optimization:

    module.exports = {
      mode: 'production'
    };
Enter fullscreen mode Exit fullscreen mode

Alternatively, you can pass it as a CLI argument:

webpack --mode=production
Enter fullscreen mode Exit fullscreen mode

Doing this will limit optimizations, such as minification or removing development-only code, to libraries. It will not expose source code, file paths, and many more.

5.Dependency optimization

When considering optimizing the application bundle size, it’s worth checking how much code you are actually utilizing from dependencies. For example, you could be using Moment.js which includes localized files for multi-language support. If you don’t need to support multiple languages, then you can consider using moment-locales-webpack-plugin to remove unused locales for your final bundle.

Another example is loadash. Let’s say you are only using 20 of the 100+ methods, then having all the extra methods in your final bundle is not optimal. So for this, you can use lodash-webpack-plugin to remove unused functions.

Here is an extensive list of dependencies which you can optimize.

6. Use React.Fragments to Avoid Additional HTML Element Wrappers

React.fragments lets you group a list of children without adding an extra node.

class Comments extends React.PureComponent{
    render() {
        return (
            <React.Fragment>
                <h1>Comment Title</h1>
                <p>comments</p>
                <p>comment time</p>
            </React.Fragment>
        );
    } 
}
Enter fullscreen mode Exit fullscreen mode

But wait! There is the alternate and more concise syntax using React.fragments:

class Comments extends React.PureComponent{
    render() {
        return (
            <>
                <h1>Comment Title</h1>
                <p>comments</p>
                <p>comment time</p>
            </>
        );
    } 
}
Enter fullscreen mode Exit fullscreen mode

7. Avoid Inline Function Definition in the Render Function.

Since functions are objects in JavaScript ({} !== {}), the inline function will always fail the prop diff when React does a diff check. Also, an arrow function will create a new instance of the function on each render if it's used in a JSX property. This might create a lot of work for the garbage collector.

default class CommentList extends React.Component {
    state = {
        comments: [],
        selectedCommentId: null
    }

    render(){
        const { comments } = this.state;
        return (
           comments.map((comment)=>{
               return <Comment onClick={(e)=>{
                    this.setState({selectedCommentId:comment.commentId})
               }} comment={comment} key={comment.id}/>
           }) 
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

Instead of defining the inline function for props, you can define the arrow function.

default class CommentList extends React.Component {
    state = {
        comments: [],
        selectedCommentId: null
    }

    onCommentClick = (commentId)=>{
        this.setState({selectedCommentId:commentId})
    }

    render(){
        const { comments } = this.state;
        return (
           comments.map((comment)=>{
               return <Comment onClick={this.onCommentClick} 
                comment={comment} key={comment.id}/>
           }) 
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

8. Throttling and Debouncing Event Action in JavaScript

Event trigger rate is the number of times an event handler invokes in a given amount of time.

In general, mouse clicks have lower event trigger rates compare to scrolling and mouseover. Higher event trigger rates can sometimes crash your application, but it can be controlled.

Let's discuss some of the techniques.

First, identify the event handler that is doing the expensive work. For example, an XHR request or DOM manipulation that performs UI updates, processes a large amount of data, or perform computation expensive tasks. In these cases, throttling and debouncing techniques can be a savior without making any changes in the event listener.

Throttling

In a nutshell, throttling means delaying function execution. So instead of executing the event handler/function immediately, you’ll be adding a few milliseconds of delay when an event is triggered. This can be used when implementing infinite scrolling, for example. Rather than fetching the next result set as the user is scrolling, you can delay the XHR call.

Another good example of this is Ajax-based instant search. You might not want to hit the server for every key press, so it’s better to throttle until the input field is dormant for a few milliseconds

Throttling can be implemented a number of ways. You can throttle by the number of events triggered or by the delay event handler being executed.

Debouncing

Unlike throttling, debouncing is a technique to prevent the event trigger from being fired too often. If you are using lodash, you can wrap the function you want to call in lodash’s debounce function.

Here’s a demo code for searching comments:

import debouce from 'lodash.debounce';

class SearchComments extends React.Component {
 constructor(props) {
   super(props);
   this.state = { searchQuery: “” };
 }

 setSearchQuery = debounce(e => {
   this.setState({ searchQuery: e.target.value });

   // Fire API call or Comments manipulation on client end side
 }, 1000);

 render() {
   return (
     <div>
       <h1>Search Comments</h1>
       <input type="text" onChange={this.setSearchQuery} />
     </div>
   );
 }
}
Enter fullscreen mode Exit fullscreen mode

If you are not using lodash, you can use the minified debounced function to implement it in JavaScript.

function debounce(a,b,c){var d,e;return function(){function h(){d=null,c||(e=a.apply(f,g))}var f=this,g=arguments;return clearTimeout(d),d=setTimeout(h,b),c&&!d&&(e=a.apply(f,g)),e}}
Enter fullscreen mode Exit fullscreen mode

Reference and Related Articles:
"Array" Methods,
Handling Events

9. Avoid using Index as Key for map

You often see indexes being used as a key when rendering a list.

{
    comments.map((comment, index) => {
        <Comment 
            {..comment}
            key={index} />
    })
}
Enter fullscreen mode Exit fullscreen mode

But using the key as the index can show your app incorrect data as it is being used to identify DOM elements. When you push or remove an item from the list, if the key is the same as before, React assumes that the DOM element represents the same component.

It's always advisable to use a unique property as a key, or if your data doesn't have any unique attributes, then you can think of using the shortid module which generates a unique key.

import shortid from  "shortid";
{
    comments.map((comment, index) => {
        <Comment 
            {..comment}
            key={shortid.generate()} />
    })
}
Enter fullscreen mode Exit fullscreen mode

However, if the data has a unique property, such as an ID, then it's better to use that property.

{
    comments.map((comment, index) => {
        <Comment 
            {..comment}
            key={comment.id} />
    })
}
Enter fullscreen mode Exit fullscreen mode

In certain cases, it's completely okay to use the index as the key, but only if below condition holds:

  • The list and items are static
  • The items in the list don't have IDs and the list is never going to be reordered or filtered
  • List is immutable

References and Related Articles:
Consider providing a default key for dynamic children #1342,
The importance of component keys in React.js,
Why you need keys for collections in React

10. Avoiding Props in Initial States

We often need to pass initial data with props to the React component to set the initial state value.

Let's consider this code:

class EditPanelComponent extends Component {

    constructor(props){
        super(props);

        this.state ={
            isEditMode: false,
            applyCoupon: props.applyCoupon
        }
    }

    render(){
        return <div>
                    {this.state.applyCoupon && 
                    <>Enter Coupon: <Input/></>}
               </div>
    }
}
Enter fullscreen mode Exit fullscreen mode

Everything looks good in the snippet, right?

But what happens when props.applyCoupon changes? Will it be reflected in the state? If the props are changed without the refreshing the component, the new prop value will never be assigned to the state’s applyCoupon. This is because the constructor function is only called when EditPanelComponent is first created.

To quote React docs:

Using props to initialize a state in constructor function often leads to duplication of “source of truth”, i.e. where the real data is. This is because constructor function is only invoked when the component is first created.

Workaround:

  1. Don't initialize state with props which can be changed later. Instead, use props directly in the component.
class EditPanelComponent extends Component {

    constructor(props){
        super(props);

        this.state ={
            isEditMode: false
        }
    }

    render(){
        return <div>{this.props.applyCoupon && 
         <>Enter Coupon:<Input/></>}</div>
    }
} 
Enter fullscreen mode Exit fullscreen mode
  1. You can use componentWillReceiveProps to update the state when props change.
class EditPanelComponent extends Component {

    constructor(props){
        super(props);

        this.state ={
            isEditMode: false,
            applyCoupon: props.applyCoupon
        }
    }

    // reset state if the seeded prop is updated
    componentWillReceiveProps(nextProps){
        if (nextProps.applyCoupon !== this.props.applyCoupon) {
            this.setState({ applyCoupon: nextProps.applyCoupon })
        }
    }

    render(){
        return <div>{this.props.applyCoupon && 
          <>Enter Coupon: <Input/></>}</div>
    }
}
Enter fullscreen mode Exit fullscreen mode

References and Related Articles:
ReactJS: Why is passing the component initial state a prop an anti-pattern?,
React Anti-Patterns: Props in Initial State

Conclusion

There are many ways to optimize a React app, for example lazy loading components, using ServiceWorkers to cache application state, considering SSR, avoiding unnecessary renders etc.. That said, before considering optimization, it’s worth understanding how React components work, understanding diffing algorithms, and how rendering works in React. These are all important concepts to take into consideration when optimizing your application.

I think optimization without measuring is almost premature, which is why I would recommend to benchmark and measure performance first. You can consider profiling and visualizing components with Chrome Timeline. This lets you see which components are unmounted, mounted, updated, and how much time they take relative to each other. It will help you to get started with your performance optimization journey.

For more tips, head over to the Codementor Blog to read the original post, 21 Performance Optimization Techniques for React Apps.

Top comments (0)