Why is my React app slow?
A slow React app can be bottlenecked by JavaScript, rendering, network requests, or the browser itself. This guide shows how to identify the limiting factor, choose targeted fixes, and verify improvements.
Start with the symptom, not a React optimization
If you are asking “why is my react app slow?”, start by identifying which user action is slow. A blank initial screen, delayed search results, sticky scrolling, and sluggish navigation can have different causes. React may contribute, but so can oversized JavaScript bundles, sequential API requests, expensive CSS, or backend latency.
For MyDiscussions readers responsible for both implementation and delivery, the practical question is: what is blocking the next useful thing the user expects to see or do?
Avoid beginning with a blanket rollout of React.memo, a framework migration, or a state-management rewrite. Those interventions cost engineering time and can leave the actual bottleneck untouched.
Instead, choose one reproducible journey, measure it under realistic conditions, and follow the evidence across the network, JavaScript execution, React rendering, and browser layout.
Classify the slowdown before choosing a fix
“Slow” needs an operational definition. Record the affected route, device class, network conditions, dataset size, and user action.
| Symptom | Likely causes | First investigation |
|---|---|---|
| Initial page stays blank | Large bundles, client-only rendering, blocking requests | Chrome DevTools Network and Performance |
| Main content appears late | Slow server response, late data fetching, oversized images | Request waterfall and LCP element |
| Typing or clicking feels delayed | Long JavaScript tasks, broad state updates, expensive rendering | Performance trace and React Profiler |
| Tables or feeds scroll poorly | Excessive DOM nodes, layout work, heavy row components | Rendering trace and mounted row count |
| Navigation feels slow | Route chunk loading, sequential loaders, unnecessary refetching | Network trace during navigation |
| Performance degrades over time | Retained objects, listeners, timers, unbounded caches | Heap snapshots and repeated navigation |
Define success in user-visible terms
Core Web Vitals provide useful starting criteria. At the 75th percentile of visits, the commonly used “good” thresholds are:
- Largest Contentful Paint (LCP): 2.5 seconds or less.
- Interaction to Next Paint (INP): 200 milliseconds or less.
- Cumulative Layout Shift (CLS): 0.1 or less.
These measure loading, responsiveness, and visual stability; they do not cover every product requirement. An analytics application may also need an explicit target for opening a dashboard or filtering a large dataset.
Use the official Web Vitals guidance to distinguish field measurements from laboratory diagnostics. A single Lighthouse run is not evidence that every customer has a fast experience.
Establish a trustworthy measurement baseline
Development mode is useful for debugging but unsuitable for final performance judgments. It includes extra checks, and React Strict Mode can intentionally repeat certain work in development to reveal unsafe behavior.
Test an optimized production build using representative deployment settings. Include realistic compression, caching, authentication, and data volume.
For each investigation:
- Select one route and one interaction.
- Test both a fresh visit and a repeat visit.
- Record the browser, device, and build identifier.
- Apply consistent CPU and network throttling when simulating constrained devices.
- Repeat the scenario enough times to spot variability.
- Keep extensions and unrelated background activity from contaminating results.
Do not average away the affected audience. A powerful developer laptop can conceal excessive JavaScript execution that becomes obvious on an older phone.
Use complementary tools
Different tools answer different questions:
- Chrome DevTools Performance: identifies long tasks, scripting, layout, paint, and interaction timing.
- React Developer Tools Profiler: identifies costly component updates and which portions of the tree commit together.
- Chrome DevTools Network: exposes request timing, payload sizes, cache behavior, and dependency waterfalls.
- Lighthouse and PageSpeed Insights: provide loading diagnostics and, where available, aggregated real-user data.
- Sentry, Datadog RUM, or New Relic Browser: help connect production slowdowns to routes, releases, devices, and geographic regions.
Observability vendors introduce usage costs, instrumentation overhead, and privacy considerations. Sample appropriately and avoid capturing sensitive user content.
React profiling also requires care: profiling production-like workloads may require a profiling-enabled build. Do not assume every normal production bundle supports component-level profiling.
Why React rendering becomes expensive
A React render is not the same as a DOM update. React can run component functions and compare their output without making substantial DOM changes.
The problem is expensive work repeated too frequently, not simply a large render count.
State updates affect too much of the tree
When rapidly changing state lives high in the component hierarchy, updates can cause unnecessary work across unrelated UI.
Common examples include:
- A search input stored in a page-wide state object.
- A context provider combining authentication, filters, notifications, and live metrics.
- A dashboard parent updating whenever any widget receives data.
Move state closer to the components that own it. Split contexts by responsibility and update frequency. If using Redux or Zustand, use focused selectors rather than subscribing every component to an entire store.
The trade-off is architectural complexity. Excessively fragmented state can make cross-component workflows harder to understand. Split around meaningful ownership boundaries, not every individual value.
Memoization does not match the update pattern
React.memo can skip a component render when its props are unchanged, but newly created objects, arrays, and functions can defeat that comparison. Context changes and the component’s own state can still trigger rendering.
Use memoization when profiling shows that:
- A component is expensive to render.
- Its parent updates frequently.
- Its props often remain unchanged.
- Comparing those props costs less than rendering again.
useMemo caches a calculation; useCallback stabilizes a function reference. Neither automatically makes an application faster.
Custom comparison functions can cost more than the rendering they avoid and can introduce stale behavior if they compare props incorrectly. Follow the official React memo documentation rather than treating memoization as a universal requirement. Projects using React Compiler may already receive automatic memoization, but still need measurement.
Large lists exceed the browser’s practical budget
Rendering thousands of rich rows creates costs beyond React: DOM construction, style calculation, layout, paint, and memory consumption.
Consider TanStack Virtual or react-window to render a visible window of items. Use pagination when users do not need continuous access to the full dataset.
Virtualization has trade-offs:
- Variable-height rows require additional measurement.
- Keyboard navigation and accessibility need deliberate testing.
- Browser find and printing may not include unmounted content.
- Scroll-position restoration becomes more complicated.
Use stable item IDs as keys. Index keys in reorderable lists can associate state with the wrong item, while random keys force remounting and discard useful component state.
Why initial loading and navigation feel slow
Too much JavaScript must load and execute
Bundle size affects download time, but parsing, compilation, and execution also matter. Fast networking does not eliminate CPU costs.
Inspect bundle composition using webpack-bundle-analyzer, source-map-explorer, or a compatible Rollup/Vite visualizer. Look for duplicated libraries, unused locales, large editors, charting packages, and dependencies imported into shared entry points.
Prioritize:
- Route-level code splitting.
- Lazy loading genuinely optional features.
- Removing duplicate dependencies.
- Narrow imports where a package supports them.
- Keeping server-only dependencies out of browser bundles.
Excessive splitting can create request waterfalls and visible loading states. A lazily loaded editor is sensible; lazily loading every small control usually is not.
Prefetch likely next routes when the user’s intent is clear, but consider mobile data usage and whether speculative downloads compete with essential resources.
Data fetching starts too late
A common waterfall looks like this: download JavaScript, mount the page, fetch the account, mount a child, then fetch its records.
Where requests are independent, start them together. Framework loaders in React Router or server-side data fetching in Next.js can move fetching earlier. TanStack Query can coordinate caching, deduplication, and background refresh.
Caching introduces freshness decisions. Define which data may be briefly stale and which changes require immediate invalidation.
Server rendering can improve initial content delivery, but it does not remove backend latency or all client-side work. Hydration still has a cost where interactive client components are involved. React Server Components can reduce browser JavaScript for suitable components, but introduce server/client boundaries and framework-specific operational considerations.
For implementation details, consult the official Next.js lazy-loading guide.
Check work outside React
A fast component tree can still produce a slow page.
Main-thread computation blocks interaction
Sorting a large dataset, parsing a substantial document, or performing synchronous validation inside an input handler can prevent the browser from responding.
Options include:
- Reduce the amount of data processed.
- Precompute indexes or derived structures.
- Break suitable work into smaller tasks.
- Move heavy computation into a Web Worker.
- Perform filtering or aggregation on the server.
Workers cannot directly manipulate the DOM and may incur data-transfer costs. Server processing adds network dependency.
startTransition and useDeferredValue can keep urgent updates responsive while less urgent React work proceeds. They do not make calculations intrinsically cheaper or move arbitrary synchronous computation off the main thread.
Layout, assets, and third-party scripts dominate
Inspect traces for repeated layout or paint activity. Alternating DOM reads and writes can cause forced layout. Large images, expensive visual effects, and poorly contained animations can dominate even when React updates are short.
Also test without analytics, chat, experimentation, and embedded-media scripts. Remove them individually to establish causality rather than assuming all third-party code is equally harmful.
If memory grows after repeated navigation, investigate cleanup of subscriptions, event listeners, timers, and retained data. Do not diagnose a leak solely because memory fails to fall immediately; garbage collection is not instantaneous.
A step-by-step React performance troubleshooting process
1. Reproduce a specific user complaint
Write a scenario such as: “On the orders page with a representative large account, typing into the filter delays visible input.”
Capture the route, account characteristics, build, and browser. Avoid using private production data in an unsafe test environment.
2. Record the full journey
Start a Performance recording before the action. Keep the Network panel available and capture React profiling separately if simultaneous instrumentation introduces too much overhead.
Determine whether the delay happens before data arrives, during JavaScript work, during React updates, or after them in layout and paint.
3. Form one testable hypothesis
Examples include:
- Every keystroke filters and renders the complete dataset.
- A chart library blocks the initial route.
- Independent requests execute sequentially.
- A shared context update refreshes unrelated widgets.
A useful hypothesis predicts what should disappear or shrink in the next trace.
4. Apply the smallest relevant fix
For the filter example, keep input state local, measure filtering separately, and virtualize results if row rendering dominates. If calculation dominates, consider indexing, server filtering, or a worker.
Do not automatically apply every possible optimization.
5. Verify performance and correctness
Repeat the same test conditions. Compare user-visible timing and the specific work targeted.
Also check accessibility, stale data, loading states, memory behavior, and interaction correctness. A faster interface that drops updates or breaks keyboard access is not a successful fix.
6. Roll out with ownership and regression checks
Release gradually when risk warrants it. Compare production behavior by route and device class, not just a global average.
Assign an owner, document the root cause, and add suitable safeguards: bundle budgets, representative interaction tests, or route-level monitoring. Lab automation helps detect regressions, while real-user monitoring confirms whether customers benefit.
Common mistakes and prioritization trade-offs
Avoid these recurring traps:
- Optimizing development-only symptoms: verify production impact first.
- Counting renders instead of measuring duration: many cheap renders can be harmless.
- Using effect chains for derived state: calculate from existing state when possible instead of triggering additional update cycles.
- Debouncing every interaction: debounce network searches where useful, not the input value users expect to see immediately.
- Treating Lighthouse scores as the objective: optimize actual journeys and affected audiences.
- Rewriting before profiling: changing frameworks does not automatically fix expensive computation or slow APIs.
- Ignoring correctness costs: caching and custom memoization require invalidation and dependency discipline.
For decision-makers, prioritize by user impact, evidence strength, implementation effort, and regression risk. Removing an unnecessary waterfall can outperform a broad rendering refactor with much less delivery risk.
For related troubleshooting workflows, browse more Problems and fixes topics.
Frequently asked questions
Why is my React app slow only in development?
Development builds include diagnostics and additional overhead. Strict Mode can repeat certain work to expose bugs. Compare against an optimized production build, but investigate unintended side effects rather than disabling Strict Mode merely to hide them.
Should I add useMemo and useCallback everywhere?
No. They add dependency-management and maintenance costs, and may provide little benefit. Use them for measured expensive calculations or stable references that enable meaningful downstream optimization. Verify the result with profiling.
Will switching to Next.js fix a slow React app?
Not automatically. Next.js offers server rendering, routing, and server/client composition options that can improve loading. It will not inherently fix broad state subscriptions, huge client-side lists, inefficient calculations, or a slow database.
How can I tell whether React or my API is responsible?
Inspect the request waterfall alongside a Performance trace. If the interface waits on a pending request while the main thread is mostly idle, investigate network or backend latency. If data has arrived but substantial scripting, rendering, or layout delays the next useful screen, investigate client-side work. Both bottlenecks can coexist.
Ask the community and get answers from practitioners.