June 12, 2025

Performance Optimization in React: Real-World Examples

Introduction

React is powerful, but it can suffer performance issues when components re-render unnecessarily or handle more work than needed.

This guide explores 6 common performance bottlenecks in real-world applications — along with practical techniques to address them. Each example includes before-and-after code and clear explanations of what’s happening, why it matters, and how to optimize it effectively.


1. Unnecessary Re-renders in Child Components

Consider a simple Counter component that displays a number. Even if only the input field is updated, the Counter still re-renders - a common inefficiency in many apps.

Before Optimization

function Counter({ count }) {
  console.log("Counter rendered");
  return <h2>Count: {count}</h2>;
}

function App() {
  const [count, setCount] = React.useState(0);
  const [text, setText] = React.useState("");

  return (
    <div>
      <Counter count={count} />
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <input value={text} onChange={(e) => setText(e.target.value)} />
    </div>
  );
}

After Optimization (using react-window)

const Counter = React.memo(function Counter({ count }) {
  console.log('Counter rendered');
  return <h2>Count: {count}</h2>;
});

What's happening?
Every time the App component re-renders - even when updating the text input — the Counter component also re-renders.
Why it's a problem?
React re-renders all child components by default, even if their props haven’t changed. This adds up fast in large trees.
Optimization Approach
Use React.memo to prevent unnecessary re-renders by memoizing the component unless its props actually change.

2. Rendering Huge Lists Without Virtualization

Rendering thousands of DOM nodes can completely freeze your browser. If you ever render a long list — this is critical.

Before Optimization

function App() {
  const items = Array.from({ length: 10000 }, (_, i) => `Item ${i}`);

  return (
    <div style={{ height: '400px', overflowY: 'auto' }}>
      {items.map((item) => (
        <div key={item}>{item}</div>
      ))}
    </div>
  );
}

After Optimization (using react-window)

import { FixedSizeList as List } from 'react-window';

function App() {
  const items = Array.from({ length: 10000 }, (_, i) => `Item ${i}`);

  return (
    <List
      height={400}
      itemCount={items.length}
      itemSize={35}
      width={'100%'}
    >
      {({ index, style }) => (
        <div style={style}>{items[index]}</div>
      )}
    </List>
  );
}

What's happening?
All 10,000 list items are rendered and pushed into the DOM at once.

Why it's a problem?
Rendering thousands of DOM nodes is memory-intensive and causes the browser to lag or freeze.

How to fix it?
Use virtualization (react-window) to render only visible items and reuse DOM nodes during scroll. Smooth performance, minimal overhead.


3. Function Re-created on Every Render

Every render creates a new function instance (e.g., the onClick handler in a button). This can cause unnecessary re-renders of child components, especially when they are memorized.

Before Optimization

function App() {
  const [count, setCount] = React.useState(0);
  return <Button onClick={() => setCount(count + 1)} />;
}

After Optimization

const handleClick = React.useCallback(() => {
  setCount((prev) => prev + 1);
}, []);
return <Button onClick={handleClick} />;

What's happening?
The inline arrow function gets recreated every time the component re-renders, so it has a new reference each time.

Why it's a problem?
If the Button component is memoized, it will still re-render because the onClick prop changes by reference.

How to fix it?
Wrap the function in useCallback to keep the reference stable across renders and avoid unnecessary updates downstream.

4. Tab Content Re-renders on Every Switch

Switching between tabs causes all tab content to unmount and remount, losing state and triggering unnecessary re-renders.

Before Optimization

{activeTab === 'profile' && <Profile />}
{activeTab === 'settings' && <Settings />}

After Optimization

const tabs = useMemo(() => ({
  profile: <Profile />,
  settings: <Settings />,
}), []);
return tabs[activeTab];


What's happening?
Each tab component mounts/unmounts when switching, resetting its internal state and triggering re-renders.

Why it's a problem?
Components lose their internal state, and there's a noticeable delay on tab switches due to remounting.

How to fix it?
Store components in `useMemo` and toggle visibility instead of remounting. This keeps the component’s state and makes switching faster.


5. Uncontrolled Component Switching to Controlled

Switching an input from uncontrolled to controlled (e.g., setting its value from undefined to a real value) leads to a React warning and a forced re-render.

Before Optimization

function Input({ value }) {
  return <input value={value} onChange={() => {}} />;
}

When value is initially undefined, this becomes uncontrolled, and when it gets a value, React throws a warning and forces a re-render

After Optimization

function Input({ value }) {
  return <input value={value ?? ''} onChange={() => {}} />;
}

What's happening?
If value starts as undefined, React treats the input as uncontrolled. When it gets a value later, it switches to controlled — and React throws a warning.
Why it's a problem?
Switching from an uncontrolled to a controlled input in React causes the field to reset and displays a warning.
How to fix it?
Use a fallback value (value ?? '') to keep the input controlled from the beginning, avoiding warning and preserving expected behavior.


6. Large Components Doing Too Much

Rendering too many components at once in a large bundle can delay the first paint, making your app feel slow and unresponsive.

Before Optimization

function Dashboard() {
  return (
    <>
      <HeavyChart />
      <LiveFeed />
      <WeatherWidget />
      <NotificationPanel />
    </>
  );
}

All components are rendered and mounted at once.

After Optimization (Code Splitting)

const HeavyChart = React.lazy(() => import('./HeavyChart'));

function Dashboard() {
  return (
    <Suspense fallback={<Loader />}>
      <HeavyChart />
      {/* Other components can load similarly */}
    </Suspense>
  );
}

What's happening?
All dashboard components are loaded upfront, even if they’re not visible or immediately needed.

Why it's a problem?
It bloats the initial JavaScript bundle, slowing down your app's first render and hurting metrics like LCP.

How to fix it?
Use React.lazy with Suspense to load components only when you need them. This helps the app load faster and feel more responsive.


Final Thoughts

Performance issues in React often creep in silently - small inefficiencies that add up as your app grows. These optimization patterns are widely applicable and can help improve performance and user experience across React applications of all sizes.

No comments:

Post a Comment