HomeBytesheetsRoadmapsUI TemplatesUI ToolsStack Discussion
7 Topics
Chapter•105 Minutes

React Performance & Optimization

Diagnose unnecessary work and optimize rendering, memoization, code splitting, virtualization, transitions, network waterfalls, and bundle size based on evidence.

A Practical React Performance Model

Performance is not simply 'fewer renders'. A render may be cheap, while a single expensive calculation, layout operation, network waterfall, or large JavaScript bundle may dominate user experience.

Measure at the right level: initial load, interaction latency, rendering cost, network timing, memory growth, and browser main-thread work.

The first optimization is often architectural: avoid fetching data that is not needed, avoid rendering thousands of off-screen rows, split large routes, and keep state ownership narrow.

Performance Investigation
Key Takeaways
  • Optimize measured bottlenecks.
  • Performance includes network, JavaScript, rendering, and browser work.
  • Architecture often beats micro-optimizations.

Profiling with React DevTools

The React Profiler helps identify components that render, how long rendering takes, and which interactions cause work. Use it to establish a baseline before changing code.

A useful investigation asks: Which component rendered? Why did it render? How expensive was it? Did the render cause expensive DOM work? Is the user-visible latency actually coming from React?

Avoid optimizing a component simply because it appears in a profiler trace. The goal is meaningful user-facing improvement.

Key Takeaways
  • Profile representative user interactions.
  • Look for expensive work and unnecessary propagation.
  • Validate that an optimization improves the real interaction.

Memoization Strategy

Memoization is most effective when an expensive calculation or child render is repeated with the same inputs. It can also stabilize references passed to memoized children.

Do not wrap every component in React.memo or every callback in useCallback. Doing so can add comparison and mental overhead without reducing meaningful work.

First reduce unnecessary state changes and improve component boundaries. Then memoize expensive stable subtrees where profiling demonstrates value.

Memoized child
const Row = memo(function Row({ item, onSelect }) {
  return <button onClick={() => onSelect(item.id)}>{item.name}</button>;
});

const onSelect = useCallback(id => {
  setSelectedId(id);
}, []);
💡 The stable callback helps the memoized child avoid re-rendering when its other props remain equal.
Key Takeaways
  • Memoization trades computation for cache/comparison complexity.
  • Stable props matter only when consumers can benefit from stability.
  • Profile before and after.

Code Splitting & Lazy Loading

Large applications should not necessarily ship every route's code on the initial page. Dynamic imports and lazy loading can split code into chunks that are loaded when needed.

Route-level splitting is often a strong default because users commonly visit only a subset of an application's features. Suspense can provide the loading boundary for lazily loaded content.

Code splitting has tradeoffs: too many tiny chunks can create overhead, and a chunk needed immediately can become a network bottleneck. Measure real navigation patterns.

Lazy component
const ReportsPage = lazy(() => import("./ReportsPage"));

function App() {
  return (
    <Suspense fallback={<p>Loading reports…</p>}>
      <ReportsPage />
    </Suspense>
  );
}
💡 The ReportsPage module can be loaded only when the lazy component is rendered.
Key Takeaways
  • Split code around meaningful loading boundaries.
  • Use Suspense for loading UI where appropriate.
  • Avoid both one giant bundle and excessive fragmentation.

Virtualized Lists

Rendering 50,000 DOM rows is expensive even if each row is simple. Virtualization renders only the visible window plus a small buffer.

Virtualized lists require careful handling of item measurement, keyboard navigation, focus, dynamic heights, and accessibility. A virtualization library can solve much of the complexity.

Do not virtualize tiny lists just because the technique is available. It adds implementation complexity and can interfere with browser-native behavior if used carelessly.

Key Takeaways
  • Virtualization limits DOM work for very large collections.
  • Accessibility and keyboard navigation must remain correct.
  • Use it when list size makes normal rendering expensive.

Transitions & Non-Urgent Updates

Some updates are urgent because they directly respond to typing or clicking. Other updates, such as rendering a large filtered result, can be lower priority.

React's transition APIs let you communicate that an update is non-urgent. This can keep urgent input responsive while React works on the expensive result.

Transitions do not make expensive code magically cheap. They improve scheduling and responsiveness by allowing React to prioritize work.

Key Takeaways
  • Separate urgent input from expensive derived UI when appropriate.
  • Transitions improve scheduling, not raw algorithmic complexity.
  • Keep loading and pending states understandable to users.

Bundle, Network & Waterfall Optimization

A React application can feel slow before React even renders. Large JavaScript bundles, duplicate dependencies, uncompressed assets, font loading, API waterfalls, and render-blocking resources all matter.

Prefer parallel data fetching when dependencies allow it. Preload or prefetch intentionally rather than indiscriminately. Compress assets, cache immutable files, and remove unused dependencies.

Measure using browser performance tools and real-user metrics rather than relying only on local development builds.

Key Takeaways
  • Network waterfalls can dominate UI latency.
  • Analyze production bundles rather than development output.
  • Parallelize independent requests and cache appropriate resources.