Pixie is now a CNCF Sandbox project! Learn more 🚀Pixie is now a CNCF Sandbox project!
pixie logo
Blog / Pixie Team Blogs

Faster React Apps With MemoizationPermalink

Nick Lanam

September 28, 2021

You have a sluggish React web app on your hands. Why is it slow? How do you fix it? React's documentation offers some tips. In this post, we'll walk through debugging excessive re-rendering, a common performance problem that surfaced in Pixie's UI. We'll show how we fixed it, and how we're preventing it from resurfacing.

Here, "Highlight Updates When Components Render" is enabled in React DevTools. Each rectangular flash is a component update.

TL;DR: Most of our React components were re-rendering too often. Memoizing them fixed it. We used React Dev Tools and Why Did You Render to find the worst offenders, then added an eslint rule to remind us to use more memoization going forward.

Profiling With React DevToolsPermalink

To find the problem, we used the official React DevTools browser extension. It can show the full component tree, state, props, and errors. It can also take a profile of application performance to show which components are rendering, when, why, and how long they take to render.

Let's use this to profile our UI when it renders charts, tables, and graphs of data that it gets from executing a function upstream. Opening the Profiler tab in the browser's dev tools, we start a profile, trigger the problematic action in the UI, and stop the profile. After it finishes processing, we find the moment we care about:

Open the dev tools, go to the new Profiler tab, and click the record icon in the top left.
Open the dev tools, go to the new Profiler tab, and click the record icon in the top left.

...ouch. This profile shows us that almost the entire app updated! Each bar represents a React component in a given React render. Color indicates activity:

  • Gray components didn't render
  • Teal components rendered quickly
  • Yellow components rendered slowly

Generic toolbars shouldn't care what's happening in data tables elsewhere on the page. Likewise, dropdowns don't need to update while they're closed. So why did they update? Let's focus on the toolbars first:

The Nav component updated, but nothing about it should have changed.
The Nav component updated, but nothing about it should have changed.

By clicking on the Nav component, we get some more info: what caused it to render at this moment, and when else in the profile it rendered. Here, we see that a hook changed, but we're not sure which hook or why yet.

Why Did You Render?Permalink

React DevTools isn't providing quite the detail we need here, so let's use another tool that can: Why Did You Render (WDYR). This tool finds potentially avoidable re-renders, by logging when components update without their inputs changing. After a little setup, WDYR can be attached with SomeComponent.whyDidYouRender = true; after its definition. Doing this for Nav, we see a console message when we run our test again:

There are another few dozen of these messages out of frame. This component updated at the slightest whiff of activity
There are another few dozen of these messages out of frame. This component updated at the slightest whiff of activity

Conveniently, WDYR tells us exactly what's wrong: Nav isn't a pure component. For a handy guide on how to interpret WDYR console logs, check out this post. Note that this is a small sample of what WDYR can do; the tool is incredibly easy to configure and explains its findings clearly.

Now, why do we care if Nav is a pure component? Because unless you tell React that your component will do the same thing if it gets the same inputs twice1 (a "pure" function), React assumes it has side effects and renders it again.

This can cause a wasteful update cascade: if a component re-renders with recomputed values, then its children receive new inputs (even if they're identical) and repeat this process downward.

By contrast, React memoizes components we mark as pure: it remembers what the component rendered when given specific inputs, and recycles that output if it sees those same inputs again. This lets it skip rendering the component entirely, saving time. It can do the same thing with computed values inside a component, with hooks like useMemo 2.

Memoizing EverythingPermalink

Okay, let's throw memoization at everything3 that isn't nailed down. Using React DevTools and WDYR like above, we found the worst offenders and fixed them. For the most part, this meant wrapping various components in React.memo and computed values in React.useMemo or similar. Eventually, a typical profile when the UI starts rendering results looked like this:

Checking for changes costs far less than rendering. ~50ms saved; now the whole cycle is dedicated to actual updates.
Checking for changes costs far less than rendering. ~50ms saved; now the whole cycle is dedicated to actual updates.

Prevention Through LintingPermalink

To prevent needless re-renders from sneaking back into Pixie's UI, we added the eslint-plugin-react-memo lint rules to enforce memoization. Its author wrote a compelling article on why this is a sensible default. The rationale is simple: most of the time, your component is pure. Mark it that way unless you have a compelling reason not to. The same goes for values that don't need to be recomputed often.

SummaryPermalink

Before this investigation, Pixie's UI was full of bright-eyed, eager React components ready to update at the first sign of action. We've wisened them up to only update when something they care about changes, using popular tools and built-in features of React. With less noise blocking the main thread, the UI is much more responsive now. We then added tooling to remind ourselves to keep up this habit.

Shorter renders add up to a snappier response. This even reduced the time between clicking "Run" and the API call being sent.

Combined, the time between a user click and showing results went from ~4.1 seconds to ~2.4, a 1.7 second improvement. The network accounts for roughly 0.2 seconds in both cases.

There are further improvements we can make, of course, but this was a significant first step.

Questions? Suggestions? Find us on Slack, GitHub, or Twitter at @pixie_run.


  1. "Inputs" here means props, state, hooks including useContext, and anything else React watches.
  2. Memoizing computed values guarantees stable object identities (useful for equality checks in props).
  3. Pure components rely on stable results. Side effects like raw DOM manipulation break this assumption.

Nick Lanam

Lead SWE @ New Relic, Founding Engineer @ Pixie Labs
This site uses cookies to provide you with a better user experience. By using Pixie, you consent to our use of cookies.