Vitali Zaidman I've been a web and mobile developer since 2012, recently focused on leading the development of large React.js websites. I like to lead and tutor teams as a hands-on expert, as well as maintain open-source projects, give lectures, participate in podcasts, and write about web technologies.

Debugging React performance issues with Why Did You Render

3 min read 982

React Logo

React is lightning fast. Often, it’s fast enough to make it hard to re-create performance issues in a sandbox on our machines. Consequently, we might believe that our web app will run smoothly, no matter what we do.

But it’s a trap. Developers usually use strong machines and internet connections to build web apps. However, our high-performance environments might hide from us performance issues that can hurt our users. Unlike developers, many of our users access our web apps using mid-tier mobile smartphones from locations with bad internet connections.

But your app’s speed and performance matter and have a major effect on conversion rates.

If we don’t analyze performance, because of our relatively strong machines and internet connections, we will not know about performance issues that hurt our users and subsequently hurt our conversion rates.

This article will present a sandbox with a performance issue caused by a small React anti-pattern. Then, I’ll show you how it can be detected using the library Why Did You Render and how to resolve the issue.

Reproducing performance issues

The following app simulates a header that changes its size as the user scrolls. A list with many rows is used to represent a medium-to-large application.

Note: In practice, we would want to make such a long list a virtual list, but in our case, we only use it to simulate an application.

Our app has performance issues with the animation of the header’s height change while scrolling.

Here is the sandbox:

Performance Issue in React

Performance Issue in React by vzaidman using create-react-app, react, react-dom

To reproduce the performance issue on powerful machines, I suggest slowing the browser down artificially by throttling the CPU. You can do that in Chrome using the Performance tab.

It’s great practice to use this important tool to run your app this way when working on performance issues to see how things work on slower devices:

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

6x Slowdown Option

Can you detect what causes the performance issue in the code?

Detecting the bug with Why Did You Render

The usual way to spot the issue is by using React or Browser dev-tools. Many great articles demonstrate how to debug that in-depth.

In short, we open the browser dev-tools (in this case, Chrome’s dev-tools, but other modern browsers have similar features) and record a few seconds while reproducing the issue:

Recording

Then we stop the profiler. We can immediately see that the Main component, with its many children, re-renders on every single scroll event, causing janks in the application.

Janks in App

But before we jump to the code and try to understand what’s wrong with Main, let’s try Why Did You Render.

What is Why Did You Render?

Why Did You Render is a library created by Welldone Software that detects why a component in your app is re-rendering through monkey-patches in React and will notify you about potentially avoidable re-renders.

Note: Make sure not to add the library in production, as it slows React and even might cause it to break in certain edge-cases. Only turn it on when you debug performance issues.


First, we add the library from npm:

npm install @welldone-software/why-did-you-render --save

Next, we add a wdyr.js file to the root of our project:

import React from "react";

// Make sure to only include the library in development
if (process.env.NODE_ENV === "development") {
  const whyDidYouRender = require("@welldone-software/why-did-you-render");
  whyDidYouRender(React, {
    trackAllPureComponents: true
  });
}

And third, we import wdyr.js as the first import of our application in index.js:

import "./wdyr"; // <-- first import

import React from "react";
import ReactDOM from "react-dom";
...

For a detailed installation guide, look at the readme.

Here is a sandbox with the library installed. Now, if you scroll through, you will get the following information in your console:

Sandbox

  • Main was “re-rendered because of props changes”
  • The prop in question is style
  • style has received different objects that are equal by value:
    {paddingTop: 350} !== {paddingTop: 350}
  • Main is re-rendered by App, and App gets re-rendered because of a trigger of a useState hook

As you can see, we get a very clear picture of why Main got re-rendered. Based on this information, we can infer that the problem is caused by a widespread React anti-pattern when dealing with pure components.

Let’s look at App.js:

export default function App() {
  const headerHeight = useHeaderScroll({
    min: 50,
    max: maxHeaderHeight,
    maxOffset: 3000
  });
  return (
    <div className="App">
      <Header style={{ height: headerHeight }} />
      <Main style={{ paddingTop: maxHeaderHeight }} />
    </div>
  );
}

When a scroll happens, the hook useHeaderScroll causes App to re-render. This re-render causes the Main element to be re-created:

<Main style={{ paddingTop: maxHeaderHeight }} />

Now, because Main is a pure component, it wasn’t supposed to re-render when App re-renders, because seemingly, its props are the same as in the previous render of App. However, in reality, the style prop is a new object on every render:

{ paddingTop: maxHeaderHeight } !== { paddingTop: maxHeaderHeight }

Debugging the React app

An easy solution to the performance issue would be to pass only the relevant value to Main instead of the style object.

Let’s change:

<Main style={{ paddingTop: maxHeaderHeight }} />

To this:

<Main paddingTop={paddingTop} />

Main will no longer re-render because its only prop is always paddingTop={350}.

Now we just have to make sure that Main is adjusted accordingly to expect paddingTop as a prop instead if style:

const Main = ({ paddingTop } /* instead of {style} */) => {

You can find the corrected application without performance issues in the following sandbox.

Conclusion

Using Why Did You Render can help identify bugs in your React app very effectively, even in places one would not normally look for them. It also reports in great detail so you can know exactly what went wrong. My suggestion is to at least run your initial page load with it to see how can you speed it up in minutes. Thanks for reading.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult 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 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 — .

Vitali Zaidman I've been a web and mobile developer since 2012, recently focused on leading the development of large React.js websites. I like to lead and tutor teams as a hands-on expert, as well as maintain open-source projects, give lectures, participate in podcasts, and write about web technologies.

Leave a Reply