Skip to main content
Back to Blog
EngineeringDec 20, 2025·9 min read

Optimizing React Application Performance

Techniques for minimizing re-renders, lazy loading components, and improving your core web vitals.

ReactPerformance
React component tree performance profiler

React's reconciliation algorithm is remarkably efficient — but it only optimises within a tree it understands. When state updates cascade through poorly structured component trees, triggering unnecessary re-renders of expensive components, the result is an application that feels sluggish despite technically running on fast hardware. Most React performance problems are architectural, not algorithmic.

Diagnosing Before Optimising

Before reaching for useMemo and useCallback, profile first. React DevTools' Profiler tab shows exactly which components rendered, why they rendered, and how long they took. Sort by "Render duration" and address the outliers before attempting micro-optimisations across the entire tree. The Profiler frequently reveals that the real bottleneck is a single component making a synchronous API call, not the dozens of innocent components you were about to unnecessarily memoise.

React DevTools flame chart

Strategic Memoisation

React.memo, useMemo, and useCallback all prevent unnecessary re-computation — but each has a cost: the memoised value must be stored, and the dependency comparison must run on every render. Memoisation is only worth it when the computation cost exceeds the comparison cost. Good candidates: components that render large lists, expensive derived computations (filtering 10,000 records), and callback functions passed to deeply nested child components that would otherwise re-render unnecessarily on every parent render.

Code Splitting and Lazy Loading

The single highest-impact optimisation for initial page load is reducing the JavaScript bundle. React.lazy() with Suspense defers loading of route-level components until they're navigated to. Next.js handles this automatically per-page, but in-page lazy loading of heavy modals, charts, or rich text editors can significantly reduce the initial bundle. The rule: if a component isn't visible on initial load, it shouldn't be in the initial bundle.

  • Virtualise long lists: Rendering 10,000 DOM nodes is slow; @tanstack/virtual renders only the visible rows.
  • State colocation: Move state as close to where it's used as possible. Global state updates re-render every subscriber.
  • Concurrent features: Wrap non-urgent state updates in startTransition() to keep the UI responsive during heavy renders.