Your React App is Slow. Here’s How to Actually Fix It.
A junior engineer on my team recently came to me, proud that he’d "optimized" a slow component. He'd wrapped every single child component in React.memo and every function in useCallback. The result? The code was a mess, and performance was actually a tiny bit worse. This is the most common trap I see developers fall into. They learn about React performance optimization techniques and apply them like a magic wand, without understanding the problem. Let's cut through the noise and talk about what really works.
First, Find the Bottleneck. Don't Guess.
Stop what you're doing. If you're thinking about adding useMemo somewhere, you're probably starting in the wrong place. Optimization without measurement is just fumbling in the dark. Your first and only job is to find what is actually slow.
Your best friend here is the React Profiler, built right into the React DevTools. Run it on the interaction that feels slow—like typing into a search bar or sorting a large table. It generates a flame graph that shows you every component that rendered and why. Look for two things: wide, flat bars (components that take a long time to render themselves) and a massive cascade of re-renders (a small change at the top causing the whole app to repaint).
For an even more direct approach, use a library like why-did-you-render. It monkey-patches React to throw a console warning when a component re-renders for a completely avoidable reason, like receiving a new onClick function object that does the exact same thing as the last one. It’s noisy, but for a targeted debugging session, it's brilliant. It will literally tell you "this prop changed, but its value is deeply equal to the old one."
Once the Profiler or a log points you to a specific component that re-renders 50 times a second, then you have a target.
The memo, useMemo, and useCallback Trap
Memoization is the first tool everyone reaches for, but it’s a scalpel, not a sledgehammer. React.memo, useMemo, and useCallback all work on the same principle: they cache something (a component, a complex calculation, a function) and only re-create it if its dependencies change. This prevents downstream re-renders.
The trap is that memoization has a cost. It uses memory to store the cached value and CPU to perform the dependency comparison on every single render. If the comparison is more expensive than just re-rendering the component, you've made your app slower.
So here's the honest trade-off: you should only memoize when the cost of re-rendering is high.
Is your component a massive data grid with hundreds of rows and complex logic? Good candidate for React.memo. Is it a simple Button component that just wraps an HTML element and an icon? Don't memoize it. The cost of the prop comparison will be greater than the savings from skipping a re-render.
In a FAANG interview loop, they'll often test this specific nuance. They might show you a component tree and ask where you’d apply optimizations. The senior-level answer isn't just about pointing out where to add useMemo, but also explaining where not to, and why. It shows you think about trade-offs, not just API trivia.
Your App's Structure Matters More Than memo
You can plaster useMemo all over a poorly designed component, but a better solution is to fix the architecture. The single biggest win for React performance often comes from thoughtful state management and component structure.
It's called state colocation.
Keep state as low in the component tree as you possibly can. If a single input field needs an isFocused state, that state variable belongs inside the Input component. Don't lift it up to the parent Form component unless other components absolutely need to know about it. Why? Because every time that state updates, React re-renders that component and all of its children. By keeping state local, you contain the blast radius of re-renders. A change in one input doesn't force the entire form to repaint.
If you find yourself passing props down five levels (prop drilling), that's a sign your structure might be wrong. Before reaching for Context, ask yourself: can I move the state down? Or can I compose my components differently by passing a component as a prop (e.g., <Layout left={<Sidebar />} right={<MainContent />} />)? A clean component boundary is often the best performance tool you have.
Look Beyond the Render Cycle
Sometimes your app is slow for reasons that have nothing to do with React's render-commit cycle. If your page takes five seconds to become interactive, useMemo isn't going to help. You need to look at what you're sending to the browser.
Two huge areas to investigate are list virtualization and code splitting.
For long lists—think a 1,000-item dropdown or an infinite-scroll feed—you absolutely cannot render all the items to the DOM. The browser will choke. The solution is list virtualization. Libraries like react-window and tanstack-virtual work by rendering only the handful of items currently visible in the viewport, plus a small buffer. As the user scrolls, it recycles the DOM nodes and replaces the content. This is a non-negotiable technique for any app displaying large datasets.
Code splitting is just as critical. Your initial JavaScript bundle size determines how quickly your app can be used. Use React.lazy() and <Suspense> to defer loading code for components that aren't needed right away. The analytics dashboard? The user profile editor? The comment moderation panel? Lazy load them. The user won't pay the download and parse cost for that code until they actually click the button to open that part of the app. It's one of the easiest and most impactful performance optimizations you can make.
Ready to Ace Your Next Interview?
Practice with AI-powered mock interviews tailored to your target role and company. Start Practicing for Free | Explore Interview Prep
