React performance best practices
A practical guide to diagnosing React bottlenecks, choosing the right optimizations, and enforcing performance budgets without adding unnecessary complexity.
Start with user experience, not render counts
For teams building interactive products, react performance best practices should connect engineering decisions to outcomes users notice: faster page loads, responsive controls, stable layouts, and smooth navigation. Reducing component renders is useful only when those renders are responsible for a measurable problem.
A dashboard can render frequently and remain responsive. Another application can render rarely yet freeze while parsing a large payload or initializing a chart. React is one part of a broader system that includes network delivery, JavaScript execution, data fetching, browser layout, and third-party scripts.
For MyDiscussions practitioners and decision-makers, the practical objective is to identify the dominant constraint, apply the smallest effective change, and verify the improvement under representative conditions.
Define measurable React performance criteria
Choose performance targets before choosing optimization techniques. Targets should reflect the application’s users, devices, and critical workflows.
Separate page experience from application interactions
For public pages, use Core Web Vitals as a baseline. Google’s published “good” thresholds are:
- Largest Contentful Paint, or LCP: 2.5 seconds or less.
- Interaction to Next Paint, or INP: 200 milliseconds or less.
- Cumulative Layout Shift, or CLS: 0.1 or less.
Assess these at the 75th percentile of page visits, segmented by mobile and desktop. The official Core Web Vitals documentation explains the metrics and evaluation criteria.
These thresholds do not describe every application workflow. An analytics product also needs measurements for opening a report, filtering rows, and switching organizations. INP captures responsiveness around interactions, not the full duration of every asynchronous operation.
Track both time to visible feedback and time to completed result. A loading indicator can confirm an action immediately, but it does not make a slow query fast.
Establish budgets by route and workflow
Avoid copying arbitrary bundle limits from another product. Establish a baseline, then set budgets appropriate to your audience.
| Area | Concrete criterion | Useful tools |
|---|---|---|
| Initial loading | LCP meets the target on important entry routes | PageSpeed Insights, real-user monitoring |
| Interaction | Typing, selection, and navigation remain responsive | Chrome Performance panel, INP monitoring |
| React rendering | Commits fit comfortably within the interaction budget | React Developer Tools Profiler |
| JavaScript delivery | Route-level compressed bytes stay within agreed limits | Framework bundle reports, bundle analyzers |
| Layout stability | Images, ads, and deferred panels reserve space | Lighthouse, Chrome layout-shift diagnostics |
| Reliability | Optimization does not introduce stale UI or hydration errors | Playwright, production error monitoring |
For leadership, budgets create an explicit trade-off: a new dependency or feature can consume performance headroom, but that cost should be visible during review.
Diagnose the bottleneck before changing code
Profile production-like behavior
Development mode adds overhead, and Strict Mode can intentionally repeat renders and effect setup to reveal defects. Do not treat development console output as a production benchmark.
Use a production-like staging deployment with realistic data. Test on constrained hardware or CPU throttling, not just a high-end development laptop. Include cold loads, warm navigations, and long-running sessions.
Use complementary tools:
- React Developer Tools Profiler to inspect component commits and rendering costs.
- Chrome DevTools Performance to examine JavaScript tasks, layout, painting, and network activity.
- Lighthouse for repeatable lab diagnostics.
- Sentry, Datadog RUM, or another real-user monitoring platform to identify affected routes and user segments.
React profiling support in production requires an appropriate profiling build. The official React Profiler reference describes its measurements and caveats.
Distinguish React work from browser work
A slow React commit may indicate expensive component rendering. A long task after the commit may instead come from chart initialization, synchronous storage access, or forced layout.
Similarly, poor LCP can result from a late-discovered image or slow server response even when React renders efficiently.
Optimize the largest measured contributor first. Memoizing a button cannot compensate for a large third-party script blocking the main thread.
Keep state local and updates narrow
State architecture determines how far updates propagate through the component tree.
Put state near its consumers
Keep transient interaction state close to the components that need it:
- Input text that matters only inside a form.
- Whether a tooltip or menu is open.
- Hover state for an individual card.
- Temporary selection within an isolated editor panel.
Moving these values into a root component or broad context provider can make unrelated sections render on every interaction.
Before adding memoization, consider composition. A wrapper can manage its own state while receiving expensive content through children, avoiding recreating that content solely because the wrapper changed.
Treat context as a distribution mechanism
When a context provider’s value changes, components consuming that context can render even if wrapped in memo.
Split contexts by concern and update frequency. Authentication information, theme preferences, and rapidly changing editor state rarely need the same provider value.
Memoizing a provider’s object can prevent updates caused only by a new object reference, but it will not prevent updates when a real dependency changes.
For complex shared state, Redux Toolkit with React-Redux selectors or Zustand selectors can narrow subscriptions. They also introduce conventions and maintenance costs, so adopt them for an actual state-management need rather than as automatic performance upgrades.
Use memoization selectively
React offers several mechanisms with different purposes:
memocan skip rendering a component when its props are unchanged.useMemocan reuse the result of a calculation.useCallbackcan preserve a function reference between renders.
Require a reason for every cache
Memoization is a strong candidate when:
- Profiling identifies a component or calculation as expensive.
- It executes repeatedly with unchanged inputs.
- Dependencies remain stable often enough to produce cache hits.
- Comparison and memory costs are lower than recomputation costs.
It is usually weak value for trivial calculations or components receiving fresh objects on every parent render.
A memoized table still renders if its columns prop is recreated every time. Stabilize that value when justified, or move truly static configuration outside the component.
Custom comparison functions need particular care: ignoring a changed callback can preserve stale behavior. Comparing a large nested structure may cost more than rendering.
The official React memo documentation covers these trade-offs and explains how React Compiler can reduce manual memoization when enabled. Compiler adoption does not remove the need to measure network, layout, or expensive application logic.
Render less work, not just fewer times
Virtualize genuinely large interfaces
A long list creates more than React work. Thousands of DOM nodes increase style calculation, layout, memory consumption, and accessibility complexity.
Libraries such as TanStack Virtual and react-window render a visible window plus an overscan region. Consider virtualization when users face large datasets and profiling shows mounting, scrolling, or DOM size is problematic.
Trade-offs include:
- More complicated variable-height measurement.
- Extra work for keyboard navigation and focus retention.
- Limited browser find-in-page behavior for unmounted content.
- Additional handling for printing and screen-reader navigation.
Pagination can be simpler for administrative tables. Server-side filtering is often better than downloading an entire dataset merely to virtualize its display.
Use stable keys and avoid accidental remounts
Use durable item identifiers as list keys. Array indexes are risky when items can be inserted, removed, or reordered.
Random keys force remounting, discarding component state and repeating initialization work.
Also avoid defining component functions inside another component’s render body. Their identity changes across renders, which can cause remounting when used as component types.
Move heavy computation off the interaction path
Large sorts, document parsing, and image processing can block input even with well-structured components.
Depending on the workload:
- Precompute results on the server.
- Cache derived data with bounded retention.
- Process work incrementally.
- Use a Web Worker for substantial CPU-bound operations.
Workers involve messaging and serialization overhead. They cannot manipulate the DOM directly, so keep rendering in the main thread and transfer only necessary data.
Deliver less JavaScript and fetch data deliberately
Split by route and expensive feature
Route-based code splitting is a useful default. Load a rich-text editor, map, or charting package when the relevant route or feature needs it.
Use framework support or lazy with Suspense for appropriate component boundaries. Inspect the resulting bundles to confirm that the dependency actually moved out of the initial path.
Excessive splitting can create request chains and loading flashes. Split meaningful features, not every small component.
Prefetching can improve navigation, but indiscriminate prefetching wastes bandwidth and may increase delivery costs. Prioritize likely destinations.
Prevent data waterfalls
Nested components that fetch only after mounting can create serial requests: load the shell, discover the account, fetch the project, then fetch its activity.
Prefer route loaders, server-side data access, or coordinated client queries that start independent requests concurrently.
TanStack Query and SWR provide caching and revalidation tools, but their defaults still require product-specific decisions:
- How long is cached information acceptable?
- Should focus trigger a refetch?
- Which requests can be deduplicated?
- Does navigation preserve previous data or show a skeleton?
- When should obsolete requests be cancelled?
A longer cache lifetime can improve responsiveness and reduce backend traffic, but stale permissions or financial information may be unacceptable. Client-side caching never replaces server-side authorization.
Choose server rendering for the right reasons
Next.js and React Router offer server-rendering approaches that can improve initial content delivery. React Server Components, in supporting frameworks, can keep some rendering logic and dependencies out of client bundles.
However, server rendering can add server compute, caching complexity, and hydration work for interactive regions. A client-heavy internal application may benefit less than a public content site.
Evaluate server response time, client JavaScript, cacheability, hosting cost, and operational complexity together. Keep personalized responses out of shared caches unless isolation is explicitly correct.
Keep urgent interactions responsive
Use startTransition for non-urgent state updates when urgent input should remain responsive. Use useDeferredValue when an expensive dependent view can temporarily lag behind its source value.
For example, a search field can update immediately while a result panel renders a deferred query.
These APIs prioritize React work; they do not make calculations intrinsically faster. Heavy synchronous code executed inside a transition callback still runs synchronously. Transitions also should not control the input value itself.
Debouncing serves a different purpose: it reduces operation frequency, especially network requests. A search interface may need immediate input state, debounced requests, and deferred rendering—but only if each solves a measured problem.
A step-by-step optimization process
- Select one critical workflow. Define the route, user action, device class, dataset size, and expected outcome.
- Record a baseline. Capture relevant field metrics, a browser trace, React profiling data, and route bundle size.
- Classify the bottleneck. Identify whether network, server latency, rendering, computation, layout, or third-party code dominates.
- Form one hypothesis. For example: “Typing is slow because the entire results table renders on every keystroke.”
- Apply the smallest targeted change. Localize state, narrow a subscription, virtualize rows, or defer non-urgent rendering.
- Repeat the same test. Compare multiple runs under equivalent conditions, not one favorable trace.
- Check correctness. Test focus, keyboard navigation, loading states, stale data, hydration, and error handling.
- Roll out and monitor. Verify field improvements and watch for regressions in errors, API volume, memory, and infrastructure cost.
- Document ownership. Record the budget, measurement method, and responsible team.
For distributed teams, attach before-and-after traces to pull requests. Shared evidence is more useful than arguments about whether a particular hook is “fast.”
Common mistakes that undermine React performance
- Memoizing everything: adds dependency-management complexity without guaranteed gains.
- Fetching derived state through effects: can create avoidable update chains; compute inexpensive derived values during rendering.
- Ignoring third-party scripts: analytics, chat, and experimentation tools can dominate main-thread work.
- Testing only small datasets: hides table, selector, and memory problems.
- Optimizing compressed bytes alone: smaller JavaScript can still be expensive to execute.
- Using oversized loading boundaries: replaces useful interface regions with broad spinners.
- Ignoring cache growth: long-lived sessions can accumulate data and retained objects.
- Trading away accessibility: smooth scrolling is not a win if keyboard users lose focus.
For related engineering guidance, browse more Best practices topics.
Frequently asked questions
Should every React component use memo?
No. Use it when repeated rendering is costly and props often remain unchanged. Cheap components or constantly changing props usually provide little benefit. Measure before adding custom comparisons.
Does React Compiler eliminate performance optimization?
No. It can reduce manual memoization where enabled, but it does not fix oversized payloads, request waterfalls, excessive DOM nodes, slow servers, or blocking third-party scripts.
Is server-side rendering always faster?
No. It can improve initial content delivery, but server latency and hydration can offset benefits. Compare actual routes and workloads rather than assuming one rendering architecture always wins.
What should a team optimize first?
Start with the highest-impact bottleneck in a critical user journey. Use field data to locate the problem and profiling to explain it. Prioritize repeatable user-visible improvements over lower render counts alone.
Ask the community and get answers from practitioners.