Solomon Esenyi Python/Golang developer and Technical Writer with a passion for open-source, cryptography, and serverless technologies.

Comparing tools for optimizing performance in React

8 min read 2347

Freezing web pages, sluggish UX, handling numerous components, slow rendering, and unnecessary re-rendering are common issues you might encounter when building a React application if you do not optimize your app’s performance.

This article will highlight performance optimization tools that can help you identify potential problems in your React application so you can achieve UX perfection. These tools include the Profiler API, React.memo(), and the React Developer Tools.

Contents

The Profiler API

The Profiler API (not the one from the Chrome Dev tools) is a relatively new React component developed by B. Vaughn. It provides a means to track how many times your components are re-rendered and the “cost” of rendering, i.e., the time and resources affected by that re-render.

With it, you can quickly identify slow and defecting areas of your application that may need to be optimized by memoization.

How to use the Profiler API

The Profiler API typically requires two props: the id and an onRender callback function that collects time metrics anytime a component wrapped by the <Profiler /> is mounted or updated. It is a very efficient tool for detecting lagging components in your React app.

The code snippet below shows how to profile a Footer component:

import React, { Profiler } from "react";
return(
  <App>
    <Profiler id="Footer" onRender={callback}>
      <Footer {...props} />
    </Profiler>
    <Main {...props} />
  </App>
);

You can also use multiple <Profiler/> components to keep track of different parts of your application:

return(
  <App>
    <Profiler id="Footer" onRender={callback}>
      <Footer {...props} />
    </Profiler>
    <Profiler id="Main" onRender={callback}>
      <Main {...props} />
    </Profiler>
  </App>
);

<Profiler /> components can also be nested in a manner with which you can access distinct components inside the same tree:

return(
  <App>
    <Profiler id="Panel" onRender={callback}>
      <Panel {...props}>
        <Profiler id="Main" onRender={callback}>
          <Main {...props} />
        </Profiler>
        <Profiler id="PreviewPane" onRender={callback}>
          <PreviewPane {...props} />
        </Profiler>
      </Panel>
    </Profiler>
  </App>
);

These are just different ways to use the <Profiler /> to track your application’s performance.

The onRender callback

The <Profiler/> requires an onRender method as a prop. This function runs when a component in the profiled tree “commits” a change. It gets information about what was rendered and how long it took:

We made a custom demo for .
No really. Click here to check it out.

function callback(id, phase, actualTime, baseTime, startTime, commitTime, interactions) {
  // aggregate or log render timings...
} 

Now, let’s define the props:

  • The id prop is used to identify the Profiler reporting; if you’re using several profilers, this can help you figure out what portion of the tree was compromised
  • phase(mount/update) will report whether the component tree was mounted for the first time or was re-rendered based on a change in props, state, or hooks
  • actualTime is the amount of time it took for the Profiler to mount or update its descendants
  • baseTime is the duration of the most current render time for each component
  • startTime is the timestamp when the Profiler started measuring its descendants’ mount/render time
  • commitTime is the time it took to commit an update. All profilers share this value in a commit, making it possible to group them if desired
  • interactions is a set of “interactions” tracked when the update was scheduled, for instance, when you schedule setState function

Although the API for tracking interactions is still experimental, it can become very effective when determining the cause of an update. You can learn more about it here.

import React, { Profiler } from "react";
const callback = (id, phase, actualTime, baseTime, startTime, commitTime) => {
  console.log(`${id}'s ${phase} phase:`);
  console.log(`Actual time: ${actualTime}`);
  console.log(`Base time: ${baseTime}`);
  console.log(`Start time: ${startTime}`);
  console.log(`Commit time: ${commitTime}`);
};
return (
    <>
      <Profiler id="CurrencyInput" onRender={callback}>
      <CurrencyInput
         props={props}
         />
        </Profiler>
    </>
);

In the example above, the <Profiler/> is wrapped around the CurrencyInput component enabling you to access a bunch of helpful information about this component when it mounts. In this case, you can access that information on the console:

mount phase stats

update phase stats

These parameters will aid in determining which component tree is slowing your app down and which is performing well.

Use cases for the Profiler API

You can use the <Profiler /> when:

  • You intend to retrieve timings for specific app components programmatically
  • You want to determine which specific user action or backend request caused the render to commit to being extremely slow
  • You’d like to provide your performance findings with a more semantic context

Drawbacks of the Profiler API

  • It does not necessarily give you as much render information as the React DevTools Profiler
  • It comes at a cost; each Profiler instance in your components tree incurs a tiny performance penalty

The Profiler API was also very recently disabled in production builds due to its minor impact on performance. While this isn’t usually a concern because most serious performance issues will be noticeable in production and development builds, that isn’t always the case. Sometimes you may want to access your component’s rendering time on the production bundle.

React.memo()

React.memo() is a function that tends to come in handy when handling unnecessary re-rendering. It helps to improve the rendering efficiency of functional components and hooks by making sure React skips the re-rendering of a component if its props have not changed on mounting/updating. The memo stands for memoization.

How to use React.memo()

Wrapping your function component in the React.memo() function is the most common way to use it:

const Stone = React.memo(() => {
  return (
    <div className="black-stone">
      <Task />
    </div>
  );
});

Consider the example below:

const Component1 = () => {
  console.log("Component 1 rendered")
  return (
    <>
      <p>Component 1</p>
      </>
  )
}
const Component2 = () => {
  console.log("Component 2 rendered")
  return (
    <div>
      <p>Component 2</p>
      </div>
  )
}
const SampleApp = () => {
  const [counter, setCounter] = React.useState(0)
  return (
    <div>
      <div>Count: {counter}</div>
      <Component1 />
      <Component2 />
      <button onClick={() => setCounter(counter + 1)}>increase count</button>
  </div>
  )
}

The code above is a simple React application containing two components: Component1 and Component2, which are housed by the App component. These two components will report to the console whenever rendered or re-rendered.

The App component also has a counter state variable that dynamically changes on the click of the increase count button, causing Component1 and Component2 to re-render.

components reporting rendering

After clicking the button four times (increasing the count to four), you can see that we have five logs for each component: one for the initial render and an extra four for every time the button is clicked (i.e., every time these components are re-rendered). This is a performance problem that you may be inclined to ignore as it may go unnoticed in smaller projects, but it comes to play in much larger ones, making your app redundant and slow.

The good news is that you can quickly fix this problem with React.memo(). For instance, if you do not want Component1 to re-render when you click the button, here’s how:

const Component1 = React.memo(function Component1(props) {
  console.log("Component 1 rendered")
  return (
    <div>
      <p>Component 1</p>
      </div>
  )
});

Now that we have wrapped Component1 with React.memo(), let’s head back to the console and see how that affects our app:

components rendering with react-memo

As you can see, Component1 doesn’t re-render when the counter is increased, thereby fixing our problem.

Use cases for React.memo()

It is best to use React.memo() when:

  • The component rendering time is more than 100ms
  • The component keeps re-rendering for the same set of props, which often happens when a parent component forces its child component to render
  • You are dealing with larger applications with a high number of UI components, and a re-render would result in noticeable response latency for the user (poor UX)

Drawbacks of React.memo()

Avoid using React.memo() when the performance benefits are impossible to evaluate. If the computational time for re-rendering our component (with and without this higher-order component) is small or non-existent, then utilizing React.memo() is pointless.

While it is feasible to include wrap class components with React.memo, it is considered bad practice, so it is highly discouraged. Instead, extending the PureComponent class is more desirable, which is a much cleaner way to memoize your class components.

As a rule of thumb, if you can’t measure the performance advantages, don’t use memoization.

React Developer Tools

React has a Chrome DevTools extension called React Developer Tools. The React Developer tools have two tabs: ⚛️ Components and ⚛️ Profiler.

The Components tab gives you access to your app’s component hierarchy and its state information. It displays both the root React components and the subcomponents rendered on the page.

The Profiler tab aids performance optimization because it gives you a perfect analogy of your app structure and component rendering time.

Components tab

Profiler tab

Note that you must be using React v.16.5 or higher to access React Developer Tools.

Using the Profiler

By profiling a React application, you can readily attain all the necessary data that illustrates the app’s all-around performance, which allows you to optimize it by memoizing with React.memo().

There are three easy steps to using the Profiler:

  1. Click the Record button on the Profiler tab; this gives it access to your application’s activities and its general UI behavior
  2. Carry out your app’s usual operations like you usually would (at this point, the Profiler will collect data on application re-renders)
  3. Click the Record button again to stop the recording

Interpreting the results

Typically, React runs in two phases: the render phase, which decides what DOM changes are to be made, and the commit phase, where the actions are actually carried out.

React’s Profiler collates your app’s performance information and displays them in the form of commits represented by bar charts as illustrated below.

Profiler bar chart

These charts come in three different forms.

Flame graph

This graph represents your component’s current state for a single commit. Each bar represents a component in your app, and the length of each bar is determined by its corresponding component’s render time, so the longer your component’s render time, the longer the bar becomes.

Flame graph

The app above shows that Router.Provider took longer to render than the Router.Consumer. You can click each bar to get the specific commit information for each component.

You can also get a sense of how long each component took to render by looking at the colors of the bars. The colors are interpreted as follows:

  • Gray: the component failed to render during the commit
  • Blue-green: the component comparatively took less time to render
  • Yellow-green: the component comparatively took more time to render
  • Yellow: the component took the most time to render

Ranked graph

This graph displays the results in ranked order of each component’s time to render or re-render. The component which takes the longest time is on top.

With this graph, you can immediately detect which components slow your application down and which ones impact page reloads most.

Ranked graph

The Router has the longest render time from the sample app above.

Component chart

You can access a component’s chart by double clicking any bars representing that component. This chart provides information on your component’s lifecycle during profiling time.

Component chart

As I mentioned earlier, the Profiler provides helpful information like your app’s run time, component re-render time, and commit information. These results are essential because they give you an overview of the application in different ways, enabling you to figure out which components are causing the long renders and optimize them using techniques like memoization to avoid unwanted re-renders.

It’s a piece of cake to uncover performance issues in your React application after learning how to use this Chrome DevTools plugin successfully.

Use cases for React Developer Tools

You can use React Developer Tools when:

  • You want a graphical visualization of your app’s performance, component by component
  • You want to track each component’s render time and what component is potentially causing your application to lag or freeze, especially for much larger ones
  • You want to access component and state information within the console

Drawbacks of React Developer Tools

As much as the Profiler gives you access to your app’s component information, it does not actively solve the problem, it only shows it to you. You still need to actively apply optimization techniques like memoization to improve your app’s performance.

Memoization in React terms is an optimization technique in which the child component only re-renders if the props change when the parent component re-renders. If the props remain unchanged, it will skip the render method and return the cached result.

Comparison chart

React Developer Tools Profiler The Profiler API (<Profiler/>) React.memo()
What does it do? Analyses your application and returns a graphical representation of the results. Analyses your application and produces a programmatic representation of the results. It is used to memoize components where necessary based on results gotten.
Does it display component render time? Yes Yes No
Is it effective on the production bundle? No No Yes

Looking at these tools, the React Dev Tools Profiler just edged in front for me simply because it is easy to use, well documented, and gives you full access to your component tree structure in a graph-like manner. It couldn’t be easier to identify performance “hiccups” in a React application.

Conclusion

This guide has demonstrated some of the tools available for profiling React apps and identifying performance concerns. We also went over memoization and how to use the React.memo() function to improve your app’s overall performance. Happy coding!

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Solomon Esenyi Python/Golang developer and Technical Writer with a passion for open-source, cryptography, and serverless technologies.

Leave a Reply