Engineering Note
Performance

Optimizing React Performance

Beyond the Obvious

13 min read
AdvancedPerformance

Introduction

React performance issues rarely come from React itself. They usually come from how components are structured, how state is managed, and how often unnecessary re-renders happen.

In real projects, performance problems show up as slow UI updates, laggy interactions, delayed input response, and components re-rendering even when nothing meaningful has changed.

This note focuses on practical React performance decisions, especially the patterns that improve rendering behavior, reduce repeated work, and make large interfaces feel smoother.

The Problem

A common issue is unnecessary re-rendering. When a parent component updates, child components can re-render even if their actual data has not changed.

parent.tsx
function Parent({ count }) {
  return <Child value={count} />;
}

Even if the value stays the same, child components may still re-render because the parent updated. In small components this may not matter, but in large interfaces it can create visible lag.

Common Failures

  • Unnecessary re-renders increase CPU usage
  • Large component trees amplify small performance issues
  • Expensive calculations run again on every render
  • Function and object references change too often

User Impact

  • Inputs feel delayed during typing
  • Dashboards feel heavy under frequent updates
  • Animations become less smooth
  • Users experience lag even when data is already loaded

The challenge is not to stop every render. The real goal is to reduce unnecessary repeated work while keeping the code readable and maintainable.

System Design / Approach

React performance optimization should start from measurement and component structure. Optimization works best when it targets real bottlenecks instead of adding memoization everywhere.

1. Reduce Unnecessary Re-renders

Components should update when their meaningful data changes, not just because a parent component re-rendered.

2. Stabilize References

Function references, object props, and derived values should stay stable when they are passed into memoized components.

3. Split Large Components

Smaller components isolate updates better and make performance problems easier to locate.

Implementation

Step 1: Prevent Unnecessary Re-renders

Use React.memo when a component receives stable props and does not need to re-render unless those props change.

child.tsx
const Child = React.memo(({ value }) => {
  return <div>{value}</div>;
});

The component now skips re-rendering when its props remain the same. This is useful for expensive child components, list items, and stable UI blocks.

Step 2: Stabilize Function References

Functions are recreated on every render. When passed as props, they can break memoization and trigger unnecessary updates in child components.

handlers.tsx
const handleClick = useCallback(() => {
  console.log("clicked");
}, []);

useCallback keeps the function reference stable between renders when the dependency list does not change.

Step 3: Avoid Heavy Computation in Render

Expensive calculations inside render can slow down the UI. Memoizing derived values prevents repeated computation when the input data has not changed.

computed-value.tsx
const result = useMemo(() => {
  return expensiveCalculation(data);
}, [data]);

useMemo is useful when the calculation is expensive and the result depends on a clear set of inputs.

Step 4: Split Components

Large components re-render more often and are harder to debug. Splitting components helps isolate updates and makes performance issues easier to understand.

dashboard.tsx
function Dashboard() {
  return (
    <>
      <UserProfile />
      <Notifications />
      <ActivityFeed />
    </>
  );
}

Smaller components isolate responsibility and reduce the impact of frequent state updates.

Trade-offs

Technique Benefit Cost
React.memo Prevents unnecessary re-renders when props stay stable Adds abstraction and may not help if props change frequently
useCallback Keeps function references stable for memoized children Overuse can make simple code harder to read
useMemo Avoids expensive recalculation when dependencies are unchanged Adds memory overhead and dependency management

Real-World Impact

Smoother UI

Interactions feel smoother because the interface performs less repeated rendering work.

Lower CPU Usage

Expensive updates happen less often, which helps on large dashboards and lower-powered devices.

Better Responsiveness

Inputs, filters, tables, and interactive components respond faster under frequent updates.

Key Takeaways

Optimize only when needed, not prematurely

Prevent unnecessary re-renders first

Memoization is useful but should be used carefully

Component structure impacts performance more than hooks

Future Improvements

Introduce virtualization for large lists

Use React Server Components where possible

Profile components using React DevTools