DEV Community

Hakeem Abbas
Hakeem Abbas

Posted on

Your React App Is Slow. React May Not Be the Reason.

A user clicks a button. Nothing happens for a moment. They click again. The interface finally responds. As developers, we tend to look at the most obvious suspect first: React.
Maybe the component is rendering too often. Maybe the component tree is too large. Maybe we forgot useMemo. Maybe a missing useCallback is causing a child component to render again.
So we start changing React code. Sometimes we fix the problem. Sometimes we make the code more complicated and the application feels exactly the same.
The reason is simple: “the React app is slow” doesn't tell you where the time is actually being spent. A slow interface is a symptom, not a diagnosis. Before optimizing React, you need to understand what happens between the user's interaction and the browser displaying the result.

Start With What Actually Happens

A simplified view of an interaction in a React application looks like this:
User Interaction

State Update

Render

Commit

Layout

Paint

That's already more interesting than simply saying “React renders.”
A user interaction might trigger a state update. React then renders the affected component tree and determines what needs to change. Those changes are committed to the DOM. After that, the browser may need to recalculate styles, perform layout, and paint the updated pixels. And that isn't the entire story.
JavaScript may execute before, during, or after these steps. A network request may be waiting on the critical path. A large JavaScript bundle may be delaying execution. Hydration may be consuming the main thread. A third-party script may be competing for the same resources. So when a user says, “This page feels slow,” there are many possible explanations. The mistake is assuming that React must be responsible simply because the application was built with React.

The Same Symptom Can Have Completely Different Causes

Imagine a dashboard that takes 200 milliseconds to respond when a filter is changed. You inspect the code and find that the filter updates React state. It's tempting to conclude that the render is slow. But suppose profiling shows that React takes only 10 milliseconds. The remaining 190 milliseconds could be spent somewhere else.
Maybe the filter triggers an expensive data transformation. Maybe thousands of records are being sorted on the main thread. Maybe a network request is being made. Maybe the DOM update triggers expensive layout work. Maybe a large component subtree is being hydrated. The user experiences all of these as one thing: The UI is slow. The browser, however, doesn't see one operation. It sees many.
That's why performance debugging should start by breaking the symptom into its underlying work.

Profile Before You Optimize

The first tool I reach for is usually Chrome DevTools' Performance panel. Record the interaction. Click the button. Type into the input. Open the dropdown. Perform whatever action feels slow. Then inspect the resulting timeline. You're looking for where the main thread actually spent its time.
Long JavaScript tasks are an obvious signal. So are expensive rendering operations, layout calculations, painting, and other long-running tasks. You might find something like this:

Interaction

120ms JavaScript

8ms React

15ms Layout

That's a very different problem from:

Interaction

5ms JavaScript

110ms React

10ms Layout

Both applications feel slow. But the correct fixes are completely different.
In the first case, optimizing React won't address the 120ms JavaScript operation. In the second, investigating React's rendering behavior makes much more sense. This is why profiling is more valuable than immediately reaching for an optimization API.

Unnecessary Renders Are Real, But Don't Assume They're the Problem

React can absolutely do unnecessary work. Consider a dashboard where the parent component updates because one small piece of state changes. That update may cause children to render again even though their visible output hasn't changed.
This is where tools such as React.memo can be useful. But the important question isn't: “Can I prevent this component from rendering?”
It's: “Is this render actually expensive enough to matter?” A component rendering again isn't automatically a performance problem. If a component takes 0.2ms to render, preventing that render may accomplish almost nothing.
If a component performs expensive calculations, creates a large subtree, or triggers significant downstream work, then the render becomes much more interesting.
There is another subtle issue here: changing references. If a parent creates a new object or array on every render, a child can receive a new prop reference even when the underlying data hasn't meaningfully changed. For example:

creates a new object every time the parent renders. That can affect memoization and cause additional work. But again, the correct response isn't “add memo everywhere.” First find out whether the render is actually contributing to the user-visible delay.

Sometimes the Expensive Part Is Just JavaScript

One of the easiest things to misdiagnose is expensive JavaScript running inside a component. Consider a search interface with 100,000 records. Every time the user types a character, the component filters the entire dataset and then sorts the results. React may be doing exactly what you asked it to do.
The problem is that you've asked JavaScript to perform a lot of work every time the component renders. The performance trace might look like:

Keystroke

State Update

Render

Large filter()

Large sort()

Commit

Calling this a “React performance problem” hides the actual issue. The better questions are: Can the dataset be reduced? Can the computation be avoided? Can the result be cached? Can the operation happen less frequently? Can the work move off the main thread? Can the search happen on the server instead?
The answer depends on the application, but the principle remains the same: Find the expensive operation before deciding how to optimize it.

React Can Finish Quickly and the Browser Can Still Be Slow

There is another category of performance problems that React developers often overlook: browser rendering. React commits DOM changes, but the browser still has to turn those changes into pixels. That can involve style calculation, layout, and painting.
For example, changing one element might cause the browser to recalculate the position of many other elements. Large DOM trees, complex CSS, expensive effects, animations, and forced synchronous layout can all contribute to this work.
One particularly common problem is layout thrashing. Imagine JavaScript repeatedly changing an element and then immediately asking the browser for its layout information. The browser may be forced to perform layout synchronously between those operations.
The result can be a significant amount of main-thread work. React can be perfectly optimized and the interface can still feel sluggish. A React optimization won't fix a browser layout bottleneck.

The Component Tree Isn't the Only Thing That Matters

Another common assumption is that performance problems must exist inside the component tree. They don't.
Consider a server-rendered or server-component application that still takes a noticeable amount of time before becoming interactive. Hydration can be part of the problem.
The browser may need to download JavaScript, parse it, execute it, and attach behavior to server-rendered markup.
If there is a large amount of client-side JavaScript, the user can experience delays even when the initial HTML arrives quickly. Similarly, network waterfalls can dominate the experience. One request waits for another. That request triggers another request.
A component isn't necessarily rendering slowly; the application may simply be waiting for data. The performance trace might reveal that React spent 12ms while the browser spent hundreds of milliseconds waiting on network activity.
Again, the symptoms are the same. The diagnosis isn't.

Don't Optimize the Framework. Find the Expensive Operation.

This is probably the most important habit in frontend performance work. When an application feels slow, don't start with: “Which React optimization should I use?”
Start with: “Where did the time go?” Then follow the evidence.
If the profiler shows unnecessary React renders, investigate component boundaries, state placement, props, and memoization. If JavaScript is consuming the main thread, optimize the computation. If the component tree is unnecessarily large, reduce the amount of work being rendered. If hydration is expensive, examine how much client-side JavaScript you're shipping. If the network is creating a waterfall, fix the request dependencies. If layout and paint dominate the trace, investigate browser rendering rather than React.
The optimization should follow the bottleneck. Not the other way around. A useful debugging flow is:

UI feels slow

Record a performance trace

Find where the time goes

JavaScript? React? Network? Layout?

Identify the expensive operation

Optimize that operation

Measure again

That last step matters. An optimization isn't successful because the code looks cleaner or because a familiar React API is now being used. It's successful when the measured bottleneck improves.

Performance Is a Diagnosis Problem First

React provides powerful tools for building complex interfaces, but it doesn't own everything that happens in the browser. Your application is a system. React rendering is one part of that system.
JavaScript execution, network requests, hydration, DOM updates, layout, painting, images, fonts, and third-party scripts can all affect what the user experiences.
That's why I don't like starting performance work with optimization. I start with measurement. Find the interaction that feels slow. Record it. Look at the timeline. Find the expensive operation. Then decide what needs to change.
Because if React takes 8ms and JavaScript takes 180ms, rewriting your React components isn't performance optimization. It's optimizing the wrong thing.
Your React app may be slow. React may not be the reason. The first step in performance debugging isn't finding a React optimization. It's finding where the time went.

Top comments (0)