10x improvement in React application performance

Have you encountered such a mistake? Have you tried to solve it? Have you tried looking for a solution on the net and found nothing? Usually, this problem is solved by simply reloading the page.

About a year ago in Techgoise I got the opportunity to work with a large React application. We received (inherited) a ready-made code base, made major edits and started adding new interesting features to the application.

However, we have received frequent complaints from testers and end users that they are seeing this unfortunate bug. After our analysis, we found that the reason for what is happening is that the application is consuming as much as 1.5 GB of memory!

In this article, I will tell you how we managed to reduce this figure from 1.5 GB to 150 MB, which, as a result, led to an improvement in performance by almost 10 times, and we never faced the Error again.

Finding performance bottlenecks

There are many tools and libraries for detecting bottlenecks in an application. We have tested a large number of these tools. Below are three of them that have proven to be the most useful.

1. Profiling components using an extension for Google Chrome

Flamegraph (which can be conditionally translated as a graph in the form of tongues of flame) provided by the named tool is quite informative, but its analysis takes a lot of time. It helps you determine how long it takes to render each component in your application. Colored bars let you know at a glance which component takes the longest to render. The exact time can be seen on the strip itself (if there is enough space for this) or by hovering over it. A ranked chart allows you to arrange components by render time from highest to lowest.

2. Snapshots of used memory in Firefox

This tool provides a snapshot “heaps”which is used by the current browser tab. The snapshot tree view shows the following things:

  1. Objects: Objects JavaScript and DOMsuch as functions, arrays or actually objects, as well as types DOM, such as the Window and HTMLDivElement
  2. Scripts: JavaScript sources loaded by the page.
  3. Strings.
  4. Other: internal objects used SpiderMonkey

The tool in question allows you to determine which component is using the most memory, thereby slowing down the application.

3. Package why-did-you-render

This package reports unnecessary rendering. It is highly customizable and provides detailed information about why a particular component is being re-rendered. He helped us identify key points for improvement.

The task of exhaustively analyzing the data and making all the necessary improvements seemed overwhelming, since flamegraph looked like an endless staircase. As the graph was analyzed, more and more components were added to the list for improvement.

How did we manage to solve this problem?

Gradually, we came to the conclusion that the root of all evil lies in unnecessary rendering of components. Individual components were rendered up to 50 times after page reload without any interaction with them.

After discussing the problem, we decided to act consistently and systematically, solving one small problem at a time. We looked at the smallest component and asked ourselves why is it being re-rendered if the data it uses doesn’t change? We started exploring all sorts of design patterns for improving performance and came up with the following.

1. Removing built-in functions

An inline function is a function that is defined and passed in when a component is rendered.

import Child from 'components/Child'

const Parent = () => (
 <Child onClick={() => {
   console.log('Случился клик!')
 }} />
)

export default Parent

Our code has a built-in function. There are 2 main problems associated with such functions:

  1. They trigger re-rendering of the component even if the props are the same.
  2. This, in turn, increases memory consumption.

This is mainly due to the fact that in this case the method is “passed by reference”, so on each render cycle a new function is created and the reference to it changes. This happens even when using PureComponent or React.memo()

Decision: take out the built-in functions from the rendering of the component.

import Child from 'components/Child'

const Parent = () => {
 const handleClick = () => {
   console.log('Случился клик!')
 }

 return (
   <Child onClick={handleClick} />
 )
}

This reduced the memory consumption from 1.5 GB to 800 MB.

2. Saving state in the absence of changes to the repository Redux

Typically, we use storage to store state Redux… Suppose we re-access API and we get the same data. Should we update the repository in this case? The short answer is no. If we do this, then the components using such data will be re-rendered because the data reference has changed.

The legacy codebase used a hack like this: JSON.stringify(prevProps.data) !== JSON.stringify(this.props.data)… However, on several pages, it did not have the desired effect.

Decision: before updating the state in the store Redux we carry out effective comparison of data. We found two good packages that are perfect for this task: deep-equal and fast-deep-equal

This led to a decrease in the Digit from 800 to 500 MB.

3. Conditional rendering of components

Typically, we have many components that are displayed when a button is clicked or in response to another user action on the page. These components include modals, dropdowns, tooltips, and more. Here’s an example of a simple modal window:

import { useState } from 'react'
import { Modal, Button } from 'someCSSFramework'

const Modal = ({ isOpen, title, body, onClose }) => {
 const [open, setOpen] = useState(isOpen || false)

 const handleClick =
   typeof onClose === 'function'
     ? onClose
     : () => { setOpen(false) }

 return (
   <Modal show={open}>
     <Button onClick={handleClick}>x<Button>
     <Modal.Header>{title}</Modal.Header>
     <Modal.Body>{body}</Modal.Body>
   </Modal>
 )
}

We found that many of these components were unnecessarily redrawn. Among them were huge components with a lot of child components and associated calls API

Decision: conditional rendering of such components (conditional rendering). You can also consider the option of “lazy” (lazy) loading of the code of such components.

This led to a decrease in memory consumption from 500 MB to 150 MB.

Let’s rewrite the above example:

import { useState } from 'react'
import { Modal, Button } from 'someCSSFramework'

const Modal = ({ isOpen, title, body, onClose }) => {
 const [open, setOpen] = useState(isOpen || false)

 const handleClick =
   typeof onClose === 'function'
     ? onClose
     : () => { setOpen(false) }

 // условный рендеринг
 if (!open) return null

 return (
   <Modal show={open}>
     <Button onClick={handleClick}>x<Button>
     <Modal.Header>{title}</Modal.Header>
     <Modal.Body>{body}</Modal.Body>
   </Modal>
 )
}

4. Removing unnecessary await and use Promise.all()

Most often, to work with asynchronous code, we use await and in many cases this is quite justified. However, this can lead to performance degradation when performing heavy calculations, such as saving or updating large amounts of data.

Usually, to get the initial data, we turn to API… Imagine that for initialization the application needs to receive data from 3-5 APIas in the example below. Methods get... in the example are related to the corresponding queries to API:

const userDetails = await getUserDetails()
const userSubscription = await getUserSubscription()
const userNotifications = await getUserNotifications()

Decision: for concurrent execution of requests to API should use Promise.all()note: this will only work if your data is independent of each other and the order in which it is received does not matter.

In our case, this increased the initial loading speed of the application by 30%.

const [
 userSubscription,
 userDetails,
 userNotifications
] = await Promise.all([
 getUserSubscription(),
 getUserDetails(),
 getUserNotifications()
])

The performance optimization techniques discussed in this article are just the tip of the iceberg. We will talk about more subtle tricks in one of the following articles.

Conclusion

So, to improve the performance of a React application, you must adhere to the following rules:

  1. Avoid using built-in functions. It doesn’t really matter in small applications, but as the application grows, it will negatively affect the speed of the application.
  2. Remember that data immutability is the key to preventing unnecessary rendering.
  3. For hidden components like modals and dropdowns, conditional or deferred rendering should be used. Such components are not used until a certain point, but their rendering affects performance.
  4. Whenever possible, send parallel requests to API… Their sequential execution takes much longer.

After 3 weeks of development (including testing), we finally deployed the production version of the application. Since then, we have never encountered an “Aw! Snap” error.

Thank you for your attention and have a nice day!


Cloud servers from Macleod fast and safe.

Register using the link above or by clicking on the banner and get a 10% discount for the first month of renting a server of any configuration!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *