Skip to content
Insights · Engineering

Why a React app is slow in production

Slow is one word covering five unrelated faults — a server that answers late, a first screen the browser cannot find, a blocked main thread, a render loop doing work nobody asked for, and code you did not write. Here is how to tell which one is yours before you change anything.

Scroll
EngineeringSep 15, 20268 min readBy Salman Naqvi, Founder & CEO
Why a React app is slow in production

Slow is one word covering five unrelated faults. The server answers late. The browser cannot find the thing it has to paint first. The main thread is busy at the moment the user arrives. React is re-rendering far more than the change required. Or the slow code is not yours at all. Each has a different fix, the fixes cost wildly different amounts, and choosing wrong is how a fortnight disappears into memoising components that were never the problem. What follows is the order to test them in, cheapest first.

The temptation is to skip the diagnosis, because every engineer has a favourite suspect and every favourite suspect is occasionally right. Performance is the corner of this work where intuition is least reliable and instrumentation is cheapest, and the aggregate numbers suggest the industry finds diagnosis harder than repair. The HTTP Archive's Web Almanac found 43% of mobile websites had good Core Web Vitals in its 2024 edition, up from 37% in 2023 and 31% in 2022 (HTTP Archive, Web Almanac 2024: Performance). The direction is right and the level is not: on mobile, most of the web still fails.

Two kinds of measurement exist and they answer different questions. Lab data — a trace you record on your own machine — is reproducible and tells you what is expensive. Field data is what your users actually got, on the hardware they own, and it is the only evidence that anything is wrong at all. The browser hands you the second for nothing: the PerformanceObserver interface, MDN writes, "is used to observe performance measurement events and be notified of new performance entries as they are recorded in the browser's performance timeline" (MDN, PerformanceObserver, MDN Web Docs). That is perhaps thirty lines reporting into the analytics endpoint you already run, segmented by route and by device class. Until that table exists, every performance conversation is a contest between anecdotes won by whoever owns the newest laptop.

Suspect one: the server answers late, and nothing in the component tree will help. Time to first byte covers redirects, DNS, connection and TLS negotiation and the server's own processing, and the Almanac's 2024 figures place 42% of mobile websites in the good band against 40% needing improvement and 19% poor, with 800 milliseconds as the threshold for good. That number has barely moved in five years, 41% in 2021 against 42% in 2024 — this layer is not improved by anything happening inside front-end frameworks. In a React application the cause is rarely React: it is a server-rendered route awaiting three database calls in sequence, a redirect chain in front of the page, or a cold start on a function that sees traffic every few minutes. It is the cheapest suspect to clear, because your server logs already hold the answer.

Suspect two: the browser cannot find the thing it has to paint. The largest element on the first screen usually decides the loading metric, and the browser can only start fetching it once it has seen it. The Almanac found 35% of mobile pages had a largest-contentful-paint element that was not statically discoverable in the document — an improvement on 39% in 2022 — and named three causes: lazy-loading, CSS background images, and client-side rendering. The React version is specific and extremely common. The hero image lives inside a component, so it exists only after the bundle has downloaded, parsed and executed; or it arrives from a content API inside an effect, so it exists only after a second round trip. The test takes ten seconds and is not the one people reach for: view source, not inspect element, since one shows what the server sent and the other shows what JavaScript built. If the image is absent from that HTML, the browser found out late, and the delay is on the critical path.

Suspect three: the main thread is busy at exactly the wrong moment. web.dev is precise about the unit of the problem: "Any task that takes longer than 50 milliseconds is a long task", and for tasks that exceed it, "the task's total time minus 50 milliseconds is known as the task's blocking period". The consequence is stated just as plainly — "the browser blocks interactions from occurring while a task of any length is running, but this is not perceptible to the user as long as tasks don't run for too long" (web.dev, Optimize long tasks, Google, last updated December 2024). Ordinary pages sit well over that line: the Almanac puts the median task duration at 90 milliseconds on desktop and 108 milliseconds on mobile, and notes that fewer than a quarter of websites keep it below the 50-millisecond recommendation.

Here is the number worth putting in front of a sceptical stakeholder. Median total blocking time in the Almanac's lab data was 67 milliseconds on desktop and 1,209 milliseconds on mobile, reaching 5,955 milliseconds at the 90th percentile — close to six seconds in which the page looks finished and answers nothing. Those are lab measurements on an emulated low-power device and a slow network, which the chapter says outright, so read them as the experience of a customer on a cheap phone rather than of the reviewer on a workstation. In a React app the usual occupant of that window is startup: the bundle parsing, the tree hydrating, every effect firing at once, an analytics script initialising on top. Where work genuinely has to happen there, the remedy is to hand control back between the pieces of it — the dedicated yielding API is "just a function that returns a Promise that will be resolved in a future task", and nested setTimeout is the older fallback the browser penalises after five rounds.

Suspect four: React is rendering more than the change required — and you cannot tell by reading the code. The measurement is built in. The Profiler component takes an onRender callback that React "calls every time components within the profiled tree update", and the two durations it reports are most of the diagnosis: actualDuration is "the number of milliseconds spent rendering the Profiler and its descendants for the current update", while baseDuration estimates "how much time it would take to re-render the entire Profiler subtree without any optimizations", so comparing the two tells you whether memoisation is doing anything at all (React, Profiler, react.dev). One caveat matters more than it first appears to: "Profiling adds some additional overhead, so it is disabled in the production build by default. To opt into production profiling, you need to enable a special production build with profiling enabled." You cannot profile what your users are running unless somebody decided in advance to build a version that can be.

That caveat matters because the standard non-fix is to scatter memoisation everywhere and declare the work done. React's documentation is unusually direct about the limits. "You should only rely on memo as a performance optimization. If your code doesn't work without it, find the underlying problem and fix it first." Then the scoping rule: "Optimizing with memo is only valuable when your component re-renders often with the same exact props, and its re-rendering logic is expensive." And then the trap that quietly voids most of it — memo "is completely useless if the props passed to your component are always different, such as if you pass an object or a plain function defined during rendering" (React, memo, react.dev). A codebase full of memo wrappers receiving inline objects has paid the cost of the technique and bought none of the benefit.

When a subtree is genuinely expensive and cannot be made cheap, React has a better answer than blocking on it. useDeferredValue "lets you defer updating a part of the UI", and the documentation puts it where the complaint usually lands: "it is useful when a part of your UI is slow to re-render, there's no easy way to optimize it, and you want to prevent it from blocking the rest of the UI" — prioritising the input, which must be fast, over the result list, which may be slower. It also beats the debounce most teams reach for first, because there is "no fixed delay" to pick, and "the background re-render is interruptible: if there's another update to the value, React will restart the background re-render from scratch" (React, useDeferredValue, react.dev). On a fast device the deferred render is imperceptible; on a slow one it degrades in proportion. A hard-coded debounce does neither.

Suspect five: the slow code is not yours. The Web Almanac's 2024 third-party analysis found roughly 92% of pages using at least one third party, unchanged since 2021, with a median of 66 third parties on top-thousand sites against 27 across the top million. Worse for anyone trying to hold a performance budget, what you add is not what you get: the median third-party inclusion chain runs 3.4 levels deep, 14% of chains exceed length five, and the deepest chain observed had a length of 2,930 (HTTP Archive, Web Almanac 2024: Third Parties). A tag added to a container the marketing team controls can pull in code nobody at your company has ever read. The Performance chapter's analysis of long animation frames points the same way: advertising, consent and tag-manager scripts cluster in the poor band for interactivity, while monitoring tools are among the least damaging — keep the script that measures, interrogate the rest.

The order to run it in, inside half a day. Read the field data by route and by device class; if you do not have it, collecting it is the half day. Take time to first byte from your own server logs and settle whether this is a backend problem before touching the front end at all. View source on the slowest route and find out whether the main image is in the HTML. Record a trace on a throttled profile and read the long tasks during startup, not during the interaction you happen to care about. Only then run the React profiler on a profiling build and compare actual against base duration. Finally, re-measure with the tag container emptied. The point of the sequence is that the first four steps need no React knowledge whatsoever, and they resolve the complaint more often than the fifth does.

Two related pieces cover what this one deliberately leaves out. What to check before you inherit a React codebase is the pass to run before you own an application at all, and the right document if you are quoting on somebody else's code rather than repairing your own. What a streaming AI response breaks in React is the specialised case where the render loop is under per-token pressure, which has its own answers and responds to none of the five above. The consolidation behind Cove's operator portal is where this matters commercially: three disconnected systems reduced to one operator experience with React in front of Laravel, where each new surface had to stay usable on the hardware operators actually hold.

The commercial version of all this is short. A quote to make the app faster, written without a measurement, is a quote to guess — and guessing arrives as a second invoice. Any React development company worth engaging should be able to tell you, before the contract is signed, which of the five suspects is yours and what evidence says so, and should be willing to say when the answer is that your server is slow and there is no front-end work to do. If you would rather begin from a ranked list of what is most likely to bite first across the whole system, our free production-readiness diagnostic reads a description of what you have and returns that list, saying where it is inferring rather than being told.

Find this useful? Tell Google to show you more of it.

Let's put AI to work in your business.

A 30-minute call. You bring the workflow or the roadmap — we'll tell you what's feasible, what it costs, and what we'd build first.

Book a call