Stop Using useMemo Now!

Most of the Time It Slows Down Your Application

Pavel Pogosov
JavaScript in Plain English

--

Over-optimization in React applications is an actual nightmare. Every day we have to face tons of useless optimizations, intended to make code more “mysterious”. Some developers even include useMemo and useCallback into their default style guide, just to simplify things. Don’t fall into that oblivion as useMemo can even slow down your application! Remember, memoization doesn’t come for free.

In that article, I want to show you how most developers overuse useMemo hook and give some tips on how to avoid it. When I first realized I made these mistakes, I felt foolish. Without any further words, let’s start!

Table Of Contents

Why Do We Use useMemo?

useMemo is a hook that allows us to cache calculation results between component re-renders. It’s not a secret that it is used only for performance reasons and should be used in couple with other techniques such as React.memo, useCallback, debouncing, concurrent rendering and etc.

Although in some situations this hook really helps and plays a significant role, most of the developers use it improperly. They wrap every variable into useMemo, hoping that random optimization will lead them to success. Unsurprisingly, this approach only causes bad readability and increased memory usage.

Possibly the most common error of a smart engineer is to optimise a thing that should not exist — Elon Musk

One more important thing to keep in mind is that useMemo brings value only in the rerender phase. During its initialization, memoization slows down the application and this effect has a tendency to stack up. That’s what I meant by saying “Memoization doesn’t come for free”.

When Is It Not Useful or Even Harmful?

Now, look at the examples below. All of them are real snippets of code, which I took from one of the projects I worked on. Can you say which example of useMemo actually, makes sense?

export const NavTabs = ({ tabs, className, withExpander }) => {
const currentMainPath = useMemo(() => {
return pathname.split("/")[1];
}, [pathname]);
const isCurrentMainPath = useMemo(() => {
return currentMainPath === pathname.substr(1);
}, [pathname, currentMainPath]);

return (
<StyledWrapper>
<Span fontSize={18}>
{isCurrentMainPath ? (
t(currentMainPath)
) : (
<StyledLink to={`/${currentMainPath}`}>
{t(currentMainPath)}
</StyledLink>
)}
</Span>
</StyledWrapper>
);
};

Typically we use useMemo in two cases: to remember a reference and pass it further to memoized components, or to cache some expensive calculation.

Now think for a moment, what are we optimizing in the example above? We have primitive values and don’t pass anything deeper into the component tree, so we don’t need to preserve the reference. Also .split and === don’t seem to be difficult computations to memoize. Therefore, we can easily get rid of useMemo in that example and save some space in the file :)

export const Client = ({ clientId, ...otherProps }) => {
const tabs = useMemo(
() => [
{
label: t("client withdrawals"),
path: `/clients/${clientId}/withdrawals`
},
...
],
[t, clientId]
);

...

return (
<>
...
<NavTabs tabs={tabs} />
</>
)
}

export const NavTabs = ({ tabs, className, withExpander }) => {
return (
<Wrapper className={className} withExpander={withExpander}>
{tabs.map((tab) => (
<Item
key={tab.path}
to={tab.path}
withExpander={withExpander}
>
<StyledLabel>{tab.label}</StyledLabel>
</Item>
))}
</Wrapper>
);
};

In the example above, we memoize the tabs variable and then pass it down to NavTabs. What do you think was the primary intention of optimizing tabs creation? It’s not a computation at all, so the person who implemented it probably wanted to preserve the reference and avoid excessive rerenders of NavTabs. Is it the right thing to do here?

First of all, NavTabs is a lightweight component that can be rerendered many times without affecting performance. Secondly, even if it was a heavy component, useMemo wouldn’t work. It is not enough to keep the reference to prevent NavTabs from rerendering every time the Client component does. To optimize performance, we should wrap NavTabs with React.memo.

The memo function returns a memoized version of our component. This version will usually not be re-rendered when its parent does, as long as its props have not changed. The memo uses a shallow comparison of props to decide if the component should update. You can even specify your own comparison function as a second argument to memo if you have some specific conditions under which your component should re-render.

How to tell if a calculation is expensive?

Unless you’re performing complex loops over thousands of items or calculating factorial, it’s probably not expensive. You can identify bottlenecks by using React Profiler combined withperformance.now() and only then apply optimization techniques.

Avoid using useMemo in these scenarios:

  • The calculation you’re trying to optimize is cheap. In these cases, the overhead of using useMemo would outweigh the benefits
  • When you’re not sure if you need memoization. It is better to start without it and then incorporate optimizations gradually into your code as problems occur
  • The value you’re memoizing is not passed down into components. If the value is used only in JSX and isn’t passed deeper into a component tree, most of the time you can avoid optimizations. There is no need to remember its reference as it doesn’t influence other component's rerender
  • The dependency array changes too frequently. In this case, useMemo will not give any performance benefits as it recalculates all we time

Tips on How to Use It Correctly

A React component runs its body every time it re-renders, which happens when its props or state change. Usually, we want to avoid expensive calculations during the render, as they can slow down the component.

However, most calculations are still very fast, making it difficult to understand where you actually need to use useMemo. To start incorporating optimizations effectively and purposefully, you should identify the problem first. Also, don’t forget about other techniques, such as: useCallback, React.memo, debouncing, code-splitting, lazy-loading and so on.

Let’s look at a really simple example below. doCalculation is an artificially slowed function, so it takes 500ms for it to return a random number. So what problem do we have here? Yeah, every time value updates we recalculate our heavy function and that makes it extremely difficult to use input.

function doCalculation() {
const now = performance.now();
while (performance.now() - now < 500) {
// do nothing for 500ms
}

return Math.random();
}

export default function App() {
const [value, setValue] = useState(0);
const calculation = doCalculation();

return (
<div>
<input value={value} onChange={(e) => setValue(e.target.value)} />
<h1>{`Calculation is ${calculation}`}</h1>
</div>
);
}

How can we solve this issue? Just wrap doCalculation with useMemo without any dependencies.

const calculation = useMemo(() => doCalculation(), []);

Live Example

Thanks for reading!

I hope you found this article useful. If you have any questions or suggestions, please leave comments. Your feedback helps me to become better.

Don’t forget to subscribe⭐️

More content at PlainEnglish.io.

Sign up for our free weekly newsletter. Follow us on Twitter, LinkedIn, YouTube, and Discord.

Interested in scaling your software startup? Check out Circuit.

--

--

Senior Frontend Dev, sharing my knowlege about frontend. 100% original content, 0% AI