All Articles
Frontend Architecture & Best Practices

Why Your React App Is Slow (And How to Fix It Without a Rewrite)

Most React performance problems are not framework problems. They are the same handful of issues showing up in different forms. Here is how to diagnose what is actually slow - and the fixes that work without rewriting the app.

Velox Studio12 min read

"Our React app is slow" is one of the most common things we hear from agencies and startups inheriting a codebase. The frustration is real. The fix is almost always specific and not a rewrite.

After diagnosing performance problems on dozens of React codebases, the pattern is clear: most React performance issues trace to the same handful of root causes. Different teams hit them differently, but the underlying problems are the same.

Here is the framework we use to diagnose what is actually slow - and the fixes that work without throwing out the codebase.

Step One: Find Out What "Slow" Actually Means

Before fixing anything, define what slow means in your app. "Slow" is too broad to act on.

The categories of React slowness:

Initial page load is slow. Time from URL navigation to first usable screen. Measured by Largest Contentful Paint (LCP) and First Contentful Paint (FCP). If LCP is over 2.5 seconds, this is your problem.

Interaction is slow. Time from user click to visible UI response. Measured by Interaction to Next Paint (INP). If INP is over 200ms consistently, interactions feel sluggish.

A specific screen is slow. Most of the app feels fine but one screen (often a data-heavy table or a complex form) lags. The problem is local, not global.

The app degrades over time. Performance is fine on initial load but gets worse the longer the user uses it. Usually memory leaks or accumulating state.

Specific actions are slow. One particular interaction (opening a modal, switching tabs, saving a form) is noticeably slower than others.

Each category has different root causes and different fixes. The first hour of any performance investigation should be classifying the slowness, not jumping to optimisation.

Step Two: Use the Right Tools, Not Your Intuition

The mistake most teams make is "I think this component re-renders too much, let me add useMemo everywhere." This is performance optimisation by superstition. It rarely helps and often hurts.

Use measurement tools:

Chrome DevTools Performance tab. Record an interaction, look at the flame graph. You will see exactly where time is spent. JavaScript execution, rendering, layout, painting - each is visible.

React DevTools Profiler. Records every render in the React tree. Shows which components rendered, why they rendered, and how long each render took. Often surfaces issues that DevTools Performance does not.

Lighthouse. Runs in Chrome DevTools. Gives you LCP, FCP, INP, and CLS in a single report. Use this to classify the slowness before investigating.

Web Vitals extension. Real-time Core Web Vitals readout as you use the app. Useful for spotting interactions that produce INP spikes.

The investigation should produce a specific answer: "the customer-list page re-renders 47 times when filters change" or "the Dashboard component takes 800ms to render on initial mount." Vague answers produce vague fixes.

The Real Causes of Slow React Apps

After classifying the slowness and running the profiler, here are the actual problems you will find. Listed in order of how often we see them in real codebases.

1. Re-rendering the entire app on every state change

This is the most common cause of "the app feels slow" complaints.

Symptoms: opening a modal causes the entire page to re-render. Typing in a search box re-renders the whole tree. The profiler shows hundreds of component renders for a single user action.

Root cause: state lives too high in the tree, or context is being used to share state that changes frequently, or unmemoized objects/arrays are being passed as props.

Fix: move state down to the component that needs it. Split contexts so that components only subscribe to the slice they care about. Use useMemo and useCallback for objects/arrays passed as props to memoised components.

2. The N+1 problem in data fetching

Symptoms: a list of items loads slowly. Each item shows a loading state for a noticeable time after the list itself appears.

Root cause: the list renders, then each item fetches its own data separately. 50 items = 50 network requests.

Fix: fetch the aggregated data in one request at the list level. If using React Query or similar, use a single query with the joined data. If using GraphQL, use a dataloader to batch. The pattern parallels the same issue on the backend - we covered the backend version in your Next.js app does not have a performance problem, it has a data fetching problem.

3. Large lists rendered without virtualisation

Symptoms: a table with 1000+ rows takes seconds to render or scroll. Interactions on the page are laggy while the list is mounted.

Root cause: React is rendering all 1000 rows into the DOM, even though only 20-30 are visible at once.

Fix: use a virtualisation library (react-window, react-virtual, or TanStack Virtual). Render only the visible rows. The DOM stays small and renders fast regardless of dataset size.

4. Heavy synchronous work blocking the main thread

Symptoms: clicking a button freezes the UI for 200-500ms before anything happens. The browser becomes unresponsive during specific operations.

Root cause: a large computation is running on the main thread - filtering a large array, parsing big JSON, formatting many dates, doing complex calculations in a render.

Fix: move heavy work off the main thread. Web Workers for CPU-bound calculations. useDeferredValue for low-priority React updates. useTransition for non-urgent state updates. Lazy-load heavy components with React.lazy() so they do not block initial render.

5. Bundle size that ships too much JavaScript

Symptoms: initial page load is slow even on fast connections. The Network tab shows MB of JavaScript being downloaded.

Root cause: importing entire libraries when only one function is needed. Not code-splitting routes. Including dev-only code in production builds.

Fix: bundle analyser (webpack-bundle-analyzer or @next/bundle-analyzer). Find the libraries taking the most space. Replace with smaller alternatives or import only what is needed. Code-split routes with dynamic imports. Ensure dev tools are tree-shaken in production.

6. Unmemoized expensive computations

Symptoms: the profiler shows specific components taking 100ms+ to render. The component re-renders frequently.

Root cause: expensive calculation (sorting, filtering, formatting) is happening inside the render function on every render. Even when the inputs have not changed.

Fix: wrap expensive computation in useMemo with the actual inputs as dependencies. Be deliberate - useMemo is not free. Only use it where the profiler shows real benefit.

7. The wrong data fetching strategy

Symptoms: data appears late. Multiple loading spinners chain through the page. Network waterfalls in DevTools.

Root cause: client-side data fetching when server-side would work. Sequential fetches when parallel would work. Fetching on render when fetching on route change would work.

Fix: this is its own deep topic. We covered the data fetching architecture decisions in your Next.js app does not have a performance problem, it has a data fetching problem. The summary: decide on a strategy at the architecture level, not per-component.

8. Wrong React Context usage

Symptoms: many components re-render when one slice of context changes.

Root cause: a single Context contains too many unrelated values. Any component that consumes it re-renders when any value changes.

Fix: split contexts by concern. A theme context separate from auth context separate from data context. Components subscribe only to what they need.

The Performance Anti-Patterns

These are the things teams do thinking they help, that actually do not.

Wrapping every component in React.memo. memo is not free - it has its own cost and only helps in specific situations. Use it on components that re-render frequently with the same props, not as a default.

useCallback and useMemo everywhere. Same issue. These hooks add cost. Use them when the profiler shows their absence is causing problems, not as a default.

Server-side rendering everything. SSR helps with initial paint but does not help with interaction performance. If your problem is INP, SSR will not fix it.

Switching frameworks. Most "React is slow" problems will exist in Vue, Svelte, or any other framework if the underlying patterns are wrong. The framework is rarely the bottleneck.

Adding a "performance optimisation sprint" once a year. Performance is built into architecture decisions, not bolted on later. We covered the architecture-first approach in your React codebase should not become unmanageable after 50 components.

The 80/20 Diagnosis

If you only have time to investigate three things, do these:

1. Open React DevTools Profiler, record a representative user interaction, and look at what rendered. This will show you either "everything rendered when only one thing changed" (state placement problem) or "one component took a long time" (specific component problem). Either way, you have a direction.

2. Open Chrome DevTools Performance, record the slowest user action, and look at the flame graph. This will show you whether the slowness is JavaScript execution, rendering, layout/paint, or network. Each category has different fixes.

3. Run Lighthouse and check Core Web Vitals. This will tell you whether your problem is initial load (LCP), interaction (INP), or layout stability (CLS). Each has different categories of fixes.

These three steps take under 30 minutes and produce a specific, actionable answer.

What This Means in Practice

Most slow React apps are not slow because React is slow. They are slow because architecture decisions made early in the codebase are now causing performance problems at scale.

The good news: most of these fixes are local. You change a few components, restructure a few contexts, add memoisation where it actually matters. The app becomes faster without throwing out the codebase.

The exception is when the slowness traces to fundamental architecture - state structure that does not fit the product, data fetching that is wrong at the page level, component hierarchy that re-renders too much by design. Those are bigger fixes. They still are not rewrites - they are refactors of specific layers.

If you have inherited a slow React codebase and do not know where to start, the diagnosis is the highest-leverage hour you can spend. Identify what is actually slow before you start fixing.

Need a React codebase built to perform from day one?

We build performant React and Next.js applications with architecture decisions that prevent slow apps from happening in the first place.

View React Development

Tags

React performanceReact optimizationfrontend performanceReact debuggingweb performanceReact hooksmemoization

V

Velox Studio

AI-Powered Development Studio

Share

Related Articles

Frontend Architecture & Best Practices

Your Next.js App Does Not Have a Performance Problem. It Has a Data Fetching Problem.

Most Next.js performance issues trace back to one root cause: nobody decided how data fetching would work before the first page was built. Here is the strategy we define at the start of every project.

7 min readRead Article
Frontend Architecture & Best Practices

The CSS Decisions That Determine Whether Your Site Stays Maintainable

Most CSS problems are not bugs. They are decisions made early in a project that get expensive later. Here are the decisions that separate a codebase you can ship in for years from one you have to rewrite.

8 min readRead Article
Frontend Architecture & Best Practices

Your Next.js Project Structure Is Slowing Your Team Down

Most teams start Next.js with a flat structure that works at 10 components and breaks at 100. Here is how to organise your project so it scales with your team instead of fighting it.

7 min readRead Article