Improving the Performance of Your Applications

The effective performance of a web application is critical to providing a good user experience and improving conversions. The React library implements different techniques to render our components fast and to touch the Document Object Model (DOM) as little as possible. Applying changes to the DOM is usually expensive, and so minimizing the number of operations is crucial.

However, there are some particular scenarios where React cannot optimize the process, and it's up to the developer to implement specific solutions to make the application run smoothly.

In this chapter, we will go through the basic concepts of React and we will learn how to use some APIs to help the library find the optimal path to update the DOM without degrading the user experience. We will also see some common mistakes that can harm our applications and make them slower.

We should avoid optimizing our components for the sake of it, and it is important to apply the techniques that we will see in the following sections only when they are needed.

In this chapter, we will cover the following topics:

  • How reconciliation works and how we can help React do a better job using the keys
  • Common optimization techniques and common performance-related mistakes
  • What it means to use immutable data and how to do it
  • Useful tools and libraries to make our applications run faster

Technical requirements

To complete this chapter, you will require the following:

  • Node.js 12+
  • Visual Studio Code

You can find the code for this chapter in the book's GitHub Repository at https://github.com/PacktPublishing/React-17-Design-Patterns-and-Best-Practices-Third-Edition/tree/main/Chapter10.

Reconciliation

Most of the time, React is fast enough by default, and you do not need to do anything more to improve the performance of your application. React utilizes different techniques to optimize the rendering of the components on the screen.

When React has to display a component, it calls its render method and the render methods of its children recursively. The render method of a component returns a tree of React elements, which React uses to decide which DOM operations have to be done to update the UI.

Whenever the component state changes, React calls the render method on the nodes again, and it compares the result with the previous tree of React elements. The library is smart enough to figure out the minimum set of operations required to apply the expected changes on the screen. This process is called reconciliation, and it is managed transparently by React. Thanks to that, we can easily describe how our components have to look at a given point in time in a declarative way and let the library do the rest.

React tries to apply the smallest possible number of operations on the DOM because touching the DOM is an expensive operation.

However, comparing two trees of elements is not free either, and React makes two assumptions to reduce its complexity:

  • If two elements have a different type, they render a different tree.
  • Developers can use keys to mark children as stable across different render calls.

The second point is interesting from a developer's perspective because it gives us a tool to help React render our views faster.

By default, when coming back to the children of a DOM node, both lists of children are iterated by React at the same time, and whenever there is a difference, it creates a mutation.

Let's look at some examples. Converting between the following two trees will work well when adding an element at the end of the children:

<ul>
<li>Carlos</li>
<li>Javier</li>
</ul>

<ul>
<li>Carlos</li>
<li>Javier</li>
<li>Emmanuel</li>
</ul>

The two <li>Carlos</li> trees match the two <li>Javier</li> trees by React and then it will insert the <li>Emmanuel</li> tree.

Inserting an element at the beginning produces an inferior performance if implemented naively. If we look at the example, it works very poorly when converting between these two trees:

<ul>
<li>Carlos</li>
<li>Javier</li>
</ul>

<ul>
<li>Emmanuel</li>
<li>Carlos</li>
<li>Javier</li>
</ul>

Every child will be mutated by React, instead of it realizing that it can keep the subtrees line, <li>Carlos</li> and
<li>Javier</li>, intact. This can possibly be an issue. This problem can, of course, be solved and the way for this is the key attribute that is supported by React. Let's look at that next.

Keys

Children possess keys and these keys are used by React to match children between the subsequent tree and the original tree. The tree conversion can be made efficient by adding a key to our previous example:

<ul>
<li key="2018">Carlos</li>
<li key="2019">Javier</li>
</ul>

<ul>
<li key="2017">Emmanuel</li>
<li key="2018">Carlos</li>
<li key="2019">Javier</li>
</ul>

React now knows that the 2017 key is the new one and that the 2018 and 2019 keys have just moved.

Finding a key is not hard. The element that you will be displaying might already have a unique ID. So the key can just come from your data:

<li key={element.id}>{element.title}</li>

A new ID can be added to your model by you, or the key can be generated by some parts of the content. The key has to only be unique among its siblings; it does not have to be unique globally. An item index in the array can be passed as a key, but it is now considered a bad practice. However, if the items are never recorded, this can work well. The reorders will seriously affect performance.

If you are rendering multiple items using a map function and you don't specify the key property, you will get this message: Warning: Each child in an array or iterator should have a unique "key" prop.

Let's learn some optimization techniques in our next section.

Optimization techniques

It is important to notice that, in all the examples in this book, we are using apps that have either been created with create-react-app or have been created from scratch, but always with the development version of React.

Using the development version of React is very useful for coding and debugging as it gives you all the necessary information to fix the various issues. However, all the checks and warnings come with a cost, which we want to avoid in production.

So, the very first optimization that we should do to our applications is to build the bundle, setting the NODE_ENV environment variable to production. This is pretty easy with webpack, and it is just a matter of using DefinePlugin in the following way:

new webpack.DefinePlugin({ 
'process.env': {
NODE_ENV: JSON.stringify('production')
}
})

To achieve the best performance, we not only want to create the bundle with the production flag activated, but we also want to split our bundles, one for our application and one for node_modules.

To do so, you need to use the new optimization node in webpack:

optimization: {
splitChunks: {
cacheGroups: {
default: false,
commons: {
test: /node_modules/,
name: 'vendor',
chunks: 'all'
}
}
}
}

Since webpack 4 has two modes, development and production, by default, production mode is enabled, meaning the code will be minified and compressed when you compile your bundles using the production mode; you can specify it with the following code block:

{
mode: process.env.NODE_ENV === 'production' ? 'production' :
'development',
}

Your webpack.config.ts file should look like this:

module.exports = {
entry: './index.ts',
optimization: {
splitChunks: {
cacheGroups: {
default: false,
commons: {
test: /node_modules/,
name: 'vendor',
chunks: 'all'
}
}
}
},
plugins: [
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify('production')
}
})
],
mode: process.env.NODE_ENV === 'production' ? 'production' :
'development'
}

With this webpack configuration, we are going to get very optimized bundles, one for our vendors and one for the actual application.

Tools and libraries

In the next section, we will go through a number of techniques, tools, and libraries that we can apply to our code base to monitor and improve performance.

Immutability

The new React Hooks, such as React.memo, use a shallow comparison method against the props, which means that if we pass an object as a prop and we mutate one of its values, we do not get the expected behavior.

In fact, a shallow comparison cannot find mutation on the properties and the components never get re-rendered, except when the object itself changes. One way to solve this issue is by using immutable data, data that, once it gets created, cannot be mutated.

For example, we can set the state in the following mode:

const [state, setState] = useState({})

const
obj = state.obj

obj.foo = 'bar'

setState({ obj })

Even if the value of the foo attribute of the object is changed, the reference to the object is still the same and the shallow comparison does not recognize it.

What we can do instead is create a new instance every time we mutate the object, as follows:

const obj = Object.assign({}, state.obj, { foo: 'bar' })

setState({ obj })

In this case, we get a new object with the foo property set to bar, and the shallow comparison will be able to find the difference. With ES6 and Babel, there is another way to express the same concept in a more elegant way, and it is by using the object spread operator:

const obj = { 
...state.obj,
foo: 'bar'
}

setState({ obj })

This structure is more concise than the previous one, and it produces the same result, but, at the time of writing, it requires the code to be transpiled in order to be executed inside the browser.

React provides some immutability helpers to make it easy to work with immutable objects, and there is also a popular library called immutable.js, which has more powerful features, but it requires you to learn new APIs.

Babel plugins

There are also a couple of interesting Babel plugins that we can install and use to improve the performance of our React applications. They make the applications faster, optimizing parts of the code at build time.

The first one is the React constant elements transformer, which finds all the static elements that do not change depending on the props and extracts them from render (or the functional components) to avoid calling _jsx unnecessarily.

Using a Babel plugin is pretty straightforward. We first install it with npm:

npm install --save-dev @babel/plugin-transform-react-constant-elements

You need to create the .babelrc file and add a plugins key with an array that has a value of the list of plugins that we want to activate:

{ 
"plugins": ["@babel/plugin-transform-react-constant-elements"]
}

The second Babel plugin that we can choose to use to improve performance is the React inline elements transform, which replaces all the JSX declarations (or the _jsx calls) with a more optimized version of them to make execution faster.

Install the plugin using the following command:

npm install --save-dev @babel/plugin-transform-react-inline-elements

Next, you can easily add the plugin to the array of plugins in the .babelrc file, as follows:

{
"plugins": ["@babel/plugin-transform-react-inline-elements"]
}

Both plugins should be used only in production because they make debugging harder in development mode. So far, we have learned a lot of optimization techniques and how to configure some plugins using webpack.

Summary

Our journey through performance is finished, and we can now optimize our applications to give users a better UX.

In this chapter, we learned how the reconciliation algorithm works and how React always tries to take the shortest path to apply changes to the DOM. We can also help the library to optimize its job by using the keys. Once you've found your bottlenecks, you can apply one of the techniques we have seen in this chapter to fix the issue.

We have learned how refactoring and designing the structure of your components in the proper way could provide a performance boost. Our goal is to have small components that do one single thing in the best possible way. At the end of the chapter, we talked about immutability, and we've seen why it's important not to mutate data to make React.memo and shallowCompare do their job. Finally, we ran through different tools and libraries that can make your applications faster.

In the next chapter, we'll look at testing and debugging using Jest, React Testing Library, and React DevTools.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.22.171.136