💍 React Performance, pitfalls, traps, optimisation and secrets

June 26, 2020

This article serves as a personal reminder of all the bad code I’ve written in the past and how to avoid it in the future when building React applications.

These tips are ordered in no particular way. If anything is wrong or badly explained, feel free to tweet angrily at me 🙏

Also, since standards and best practices move pretty fast (remember when redux-form was THE form management tool to use ?), this article may quickly become out of date 💃

1. The rendering engine does not live in the react package.

Yes, you read that right. It is a common misconception to think that the rendering logic is in the react package. It lives in different packages, each used for different purposes, such as react-dom or react-native for example. The react package exports tools like React.Component , React.createElement , hooks, etc… These tools are platform-agnostic, meaning that no matter the rendering target (mobile, server, web client), the same methods are usable and accessible, and you could build your own Renderer on top of React ! Your root index.js file would look something like this

import React from "react"
import MyCustomRenderer from "./myCustomRenderer"
import App from "./App"

MyCustomRenderer.render(<App />, document.getElementById("root"))

2. Treat the state as immutable

Now that you have learned that react does not handle the rendering, let’s see how you can properly use setState . While React’s setState logic is fairly straightforward, there are a few pitfalls I usually see junior developers fall in when trying to update a component state.

Consider this code

class TodoApp extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      items: [
        { text: "Learn React", id: 1 },
        { text: "Go fast", id: 2 },
      ],
    }
  }

  addEntry() {
    const entry1 = { text: "Break Things", id: this.state.items.length + 1 }
    const items = this.state.items
    items.push(entry1)
    this.setState({ items })
  }

  render() {
    return (
      <div>
        <h2>Todos:</h2>
        <ol>
          {this.state.items.map(item => (
            <li key={item.id}>
              <label>
                <span>{item.text}</span>
              </label>
            </li>
          ))}
        </ol>
        <div onClick={() => this.addEntry()}>--Add Entry--</div>
      </div>
    )
  }
}

ReactDOM.render(<TodoApp />, document.querySelector("#app"))

While this code works in practice, it has a major flaw. React’s state should be treated as immutable. Here, the push method directly modifies the items array instead of creating a new array.

Since this is a simple React.Component , the component is re-rendered every time setState is called. Let’s say we want to optimise this React.Component by replacing it with a React.PureComponent now.

class TodoApp extends React.PureComponent {
  constructor(props) {
    super(props)
    this.state = {
      items: [
        { text: "Learn React", id: 1 },
        { text: "Go fast", id: 2 },
      ],
    }
  }

  addEntry() {
    const entry1 = { text: "Break Things", id: this.state.items.length + 1 }
    const items = this.state.items
    items.push(entry1)
    this.setState({ items })
  }

  render() {
    return (
      <div>
        <h2>Todos:</h2>
        <ol>
          {this.state.items.map(item => (
            <li key={item.id}>
              <label>
                <span>{item.text}</span>
              </label>
            </li>
          ))}
        </ol>
        <div onClick={() => this.addEntry()}>--Add Entry--</div>
      </div>
    )
  }
}

ReactDOM.render(<TodoApp />, document.querySelector("#app"))

When clicking on --Add Entry-- , nothing happens, yet setState is still called. It’s because, internally, the React.PureComponent implements a function to check if it should update the component or not, thus preventing useless re-renders.

This function looks like this, you could also implement it yourself:

shouldComponentUpdate(nextProps, nextState) {
    if (this.state.items !== nextState.items) {
      return true;
    }
    return false;
  }

Since we have modified the original items array and returned it, the reference stays the same, thus returning false here and not re-rendering. An easy way to rewrite the addEntry method would be to use either .concat() which returns a new array or the ES6 spread operator like this [...this.state.items, entry1]

3. Use pure, stateless, functional components (or React.PureComponent) to prevent useless re-renders

Using a functional component instead of a class when you can helps make your code easier to read, your bundle size smaller (functions are better minified than classes) and your code more performant since it won’t update everytime the slightest state or props update happens.

Just be careful when using React.PureComponent as we have seen before that it does a shallow comparison of the state. It means that even if an array content changed, if the reference is the same, the component won’t re-render. Use a React.PureComponent when your props/state are immutable.

4. setState has asynchronous actions but is a synchronous function

Developers can easily be overwhelmed when learning about React and promises at the same time. There is often confusion about setState synchronicity. Let’s be clear: setState is a synchronous function.

When you call it, the execution is paused until setState returns. It doesn’t mean that the state is updated when the function returns. setState queues the state update and returns. If you access this.state right after calling this method, you may be reading the value before you updated the state.

Let’s consider this code:

class TodoApp extends React.PureComponent {
  constructor(props) {
    super(props)
    this.state = {
      items: [
        { text: "Learn React", id: 1 },
        { text: "Go fast", id: 2 },
      ],
    }
  }

  addEntry() {
    const entry1 = { text: "Break Things", id: this.state.items.length + 1 }
    this.setState({ items: [...this.state.items, entry1] })
    const entry2 = {
      text: "Too cool for school",
      id: this.state.items.length + 1,
    }
    this.setState({ items: [...this.state.items, entry2] })
  }

  render() {
    return (
      <div>
        {this.state.items.map(item => (
          <li key={item.id}>
            <span>{item.text}</span>
          </li>
        ))}
        <div onClick={() => this.addEntry()}>--Add Entry--</div>
      </div>
    )
  }
}

ReactDOM.render(<TodoApp />, document.querySelector("#app"))

What should be added to the list when you click on --Add Entry-- ?

Well, only Too cool for school will be added. Why ? When the first setState is executed, the function returns and the execution continues but the state isn’t necessarily updated yet. When doing the second setState , we use the spread operator to re-use the items, which have not been updated yet, thus erasing the previous state update.

Thankfully, setState provides a callback function called when the state has really been updated. Our addEntry function now looks like this:

addEntry() {
  	const entry1 = { text: "Break Things", id: this.state.items.length + 1};
    this.setState({items: [...this.state.items, entry1]}, () => {
      const entry2 = { text: "Too cool for school", id: this.state.items.length + 1};
      this.setState({items: [...this.state.items, entry2]})
    })
  }

5. Use React.fragments for a cleaner code

Ever had the following error Parse Error: Adjacent JSX elements must be wrapped in an enclosing tag ?

The easy fix is to add a <div> wrapping your code, but it can lead to all sorts of problems. An easier fix that I don’t see often enough is to wrap your code into a React.Fragment, like this:

class TodoApp extends React.PureComponent {
  render() {
    return (
      <React.Fragment>
        <h1>TodoApp</h1>
        <h2>Todos are amazing</h2>
      </React.Fragment>
    )
  }
}

ReactDOM.render(<TodoApp />, document.querySelector("#app"))

Or, even better, replace <React.Fragment> with <>

6. Do not use index as key for a map of components

I’m lazy and I assume you are too. When iterating over an array with elements to render, you sometimes do something like this, otherwise you’ll see this warning in the console Warning: Each child in an array or iterator should have a unique “key” prop.

  render() {
    return (
      {this.state.items.map((item, index) => (
        <li key={index}>
          ...
        </li>
      ))}
	  )
  }

Per the documentation:

Keys help React identify which items have changed, are added, or are removed.

I won’t go into much details about WHY you need to have a key on your items that isn’t a simple index, if you are interested you can read this post about reconciliation on Reactjs.org and this in depth article by Robin Porkony.

Simply put, it can break your application and display wrong data.

7. Lazy loading components with react.lazy

Lazy loading isn’t new, but since React 16.6.x it can be done without the use of third party libraries. React.lazy() is easy to understand: render critical UI components first, render non-critical items later, for a better user experience.

So how does it work ? Let’s see with an example:

import React, { Suspense } from 'react';

const TodoList = React.lazy(() => import('./TodoList'));

function Todo() {
  return (
    <div>
      <Suspense fallback={</loader>}>
        <TodoList />
      </Suspense>
    </div>
  );
}

By doing this, React will first load the bundle with the core components of your app. By wrapping TodoList into Suspense we can display a fallback component (here, it’s a loader) before the TodoList is finally loaded.

I have personally used this but I lack some strong real world large scale use. If you have any feedback, feel free to comment 🙂

One more thing: React.lazy() isn’t available when using Server side rendering

8. Moment, lodash and taking advantage of tree shacking in webpack

The downfall of the junior developer is to npm install everything, even huge libraries and using them like this

import lodash from 'lodash'

lodash.method()...

Most recent versions of widely used modules such as lodash are highly modular, meaning you can greatly reduce your bundle size by taking advantage of webpack ’s tree shaking feature.

import method from 'lodash/method'

method()...

This way, webpack will only include method and it’s dependencies in your bundle and not the whole freaking library.

By the way, if you are using react-native, too bad, last time I checked the metro bundler doesn’t support tree shaking.

Coming soon:

  • Garbage collector optimizations
  • Memoize everything !
  • Avoid async stuff in componentWillMount()
  • Using a CDN
  • Server-side Rendering
  • Web Workers
  • Optimizing the Webpack bundle

Hey, I'm Anton 🙌 I have no idea what I'm doing. Join me figuring things out!
If you think I have anything interesting to say, follow me on twitter
Still here ? Interested in what I do for a living ? Check out my linkedin

© 2024, Built with Gatsby because I'm too lazy making a blog by myself