Stop Memoizing Everything in React
The Temptation of React.memo()
It’s easy to get excited about React.memo(). It promises to make your React components faster by preventing unnecessary re-renders. When you see those performance gains in a benchmark, it feels like magic. So, the natural inclination is to wrap every single component in React.memo() to ensure optimal performance everywhere. I get it. I’ve been there.
But here’s the thing: React.memo() isn’t a free lunch. It comes with its own overhead, and when you apply it indiscriminately, you can actually end up hurting your application’s performance instead of helping it.
What React.memo() Actually Does
At its core, React.memo() is a Higher-Order Component (HOC). When you wrap a component with it, React will skip re-rendering the component, unless its props have changed. It does a shallow comparison of the previous props and the next props. If they are the same, React reuses the last rendered result. Simple enough, right?
However, this shallow comparison isn’t always free. For simple props like strings or numbers, it’s very fast. But what about objects, arrays, or functions? If these are recreated on every parent render (which is common in functional components), the shallow comparison will see them as changed, even if their contents are identical. This is where the perf pitfalls begin.
When React.memo() Hurts Performance
-
Comparison Overhead: For components that re-render infrequently, or components that are very cheap to render, the cost of doing the prop comparison might be more than the cost of just re-rendering the component itself. Imagine a simple
Buttoncomponent. If it renders 10 times a second, andReact.memois applied, React has to compare all its props every time. If the button’s label oronClickhandler haven’t changed,memosaves a re-render. But if they have changed, the comparison adds a little extra work before the re-render. If the component is tiny, that extra work might be more than just rendering it again. -
Stale Closures and Functions: This is a big one. Functional components often pass down callbacks. If you don’t stabilize these callbacks using
useCallback, they will be new functions on every render.React.memo()will see these new functions as prop changes and re-render the wrapped component, defeating the purpose ofmemo().function ParentComponent() {const handleClick = () => { /* ... */ };return <MyMemoizedChild onClick={handleClick} />; // handleClick is new every time}The solution here is
useCallback:function ParentComponent() {const handleClick = useCallback(() => { /* ... */ }, []); // Stable callbackreturn <MyMemoizedChild onClick={handleClick} />;} -
Complex Prop Structures: If your component receives deeply nested objects or arrays as props, and these structures are frequently updated (even partially),
React.memo()’s shallow comparison won’t help much. It will see the top-level prop object as changed and trigger a re-render anyway. You might need more sophisticated comparison logic or a different state management approach.
When Should You Use React.memo()?
So, if not everywhere, then where? Use React.memo() strategically on components that:
- Render often: Components that are part of frequently updating parts of your UI.
- Are computationally expensive to render: Components that take a noticeable amount of time to mount or update.
- Receive identical props frequently: This is key. If you know a component will often receive the exact same props (especially primitive values, stable objects, or memoized callbacks/values) across parent renders,
memocan be a win.
The Alternative: Profile First
The best approach is not to guess or to blindly apply optimizations. Use React’s built-in Profiler. It’s a tool in React DevTools that lets you record and analyze the performance of your components. It shows you which components re-rendered, why they re-rendered, and how long they took.
When you see a component causing performance issues in the Profiler, then consider using React.memo() (and useCallback/useMemo in the parent if needed). Start with components that the Profiler highlights as problematic.
My Rule of Thumb
My personal rule is to avoid React.memo() by default. I only add it when:
- I’ve identified a performance bottleneck using the Profiler.
- The bottleneck is specifically caused by unnecessary re-renders of a particular component.
- I’ve confirmed that
React.memo()(along with stabilizing parent props where necessary) actually improves performance for that specific component.
It takes a little more discipline, but it leads to cleaner, more maintainable, and genuinely faster applications. Don’t optimize prematurely. Optimize when it matters, and use the tools to find where it matters.