Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →

The React Performance Guide: Making Your App Blazing Fast

React performance optimization guide: memo, useMemo, useCallback, lazy loading, virtualization, bundle splitting, and profiling — with real examples.

A
AiTechWorlds Team
May 27, 2026 7 min read
📱

Get more content like this on Telegram!

Daily AI tips, notes & resources — free

Join Free →

The React Performance Guide: Making Your App Blazing Fast

I optimized a product page that was taking 3.2 seconds to become interactive. The culprit: a single component was re-rendering 47 times on initial load because of a chain of poorly-placed state and missing memoization.

After profiling, the fixes took 20 minutes. Time to interactive dropped to 0.9 seconds.

Performance optimization in React is not about magic — it's about understanding why React re-renders, measuring where time is actually spent, and applying targeted fixes. Premature optimization is the root of a lot of wasted effort in React codebases.

This guide gives you the mental model, the measurement tools, and the optimization techniques. In that order.


First: Understand the React Render Cycle

React re-renders a component when:

  1. Its state changes
  2. Its parent re-renders (default behavior)
  3. Its context value changes
function Parent() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      <Child />  {/* Re-renders EVERY time Parent re-renders, even though Child has no props */}
    </div>
  );
}

Re-rendering is not inherently bad — React is designed to handle many re-renders efficiently. The problem is when:

  • A component is expensive to render (complex calculations, large DOM trees)
  • A component re-renders when its props haven't changed

Step 1: Measure Before You Optimize

React DevTools Profiler shows you what actually re-renders and how long it takes:

  1. Install React DevTools browser extension
  2. Open DevTools → Profiler tab
  3. Click "Record", interact with your app, click "Stop"
  4. Inspect the flame graph — slow renders appear in red/orange

Don't skip this step. I've seen developers add useMemo everywhere to "optimize" when profiling shows the actual bottleneck is a network request or a third-party library.


Technique 1: React.memo — Skip Re-Renders

React.memo wraps a component so it only re-renders when its props actually change:

// Without memo: re-renders whenever parent re-renders
function TaskItem({ task, onToggle }: { task: Task; onToggle: (id: number) => void }) {
  console.log('TaskItem rendered:', task.id);
  return (
    <div onClick={() => onToggle(task.id)}>
      {task.text} — {task.done ? '✓' : '○'}
    </div>
  );
}

// With memo: only re-renders when task or onToggle changes
const TaskItem = React.memo(function TaskItem({ task, onToggle }) {
  console.log('TaskItem rendered:', task.id);
  return (
    <div onClick={() => onToggle(task.id)}>
      {task.text} — {task.done ? '✓' : '○'}
    </div>
  );
});

The catch: React.memo does a shallow comparison of props. If you pass a new object or function reference on every render, memo doesn't help.

function TaskList() {
  const [tasks, setTasks] = useState([...]);
  
  // NEW function created on every render — memo is useless
  const handleToggle = (id: number) => {
    setTasks(tasks.map(t => t.id === id ? { ...t, done: !t.done } : t));
  };
  
  return tasks.map(task => (
    <TaskItem key={task.id} task={task} onToggle={handleToggle} />
    // TaskItem re-renders anyway because handleToggle is new each render
  ));
}

Fix with useCallback:


Technique 2: useCallback — Stable Function References

function TaskList() {
  const [tasks, setTasks] = useState([...]);
  
  // Stable reference — same function object across renders
  const handleToggle = useCallback((id: number) => {
    setTasks(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t));
  }, []);  // Empty array: function never changes
  
  return tasks.map(task => (
    <TaskItem key={task.id} task={task} onToggle={handleToggle} />
    // Now React.memo works correctly
  ));
}

Note: handleToggle uses prev (functional state update) instead of tasks directly — this avoids capturing stale state.


Technique 3: useMemo — Cache Expensive Calculations

function ProductList({ products, searchQuery, sortBy }: Props) {
  // This filters and sorts on EVERY render
  const displayProducts = products
    .filter(p => p.name.toLowerCase().includes(searchQuery.toLowerCase()))
    .sort((a, b) => {
      if (sortBy === 'price') return a.price - b.price;
      return a.name.localeCompare(b.name);
    });
  
  // BETTER: only recalculate when products, searchQuery, or sortBy changes
  const displayProducts = useMemo(() =>
    products
      .filter(p => p.name.toLowerCase().includes(searchQuery.toLowerCase()))
      .sort((a, b) => {
        if (sortBy === 'price') return a.price - b.price;
        return a.name.localeCompare(b.name);
      }),
    [products, searchQuery, sortBy]
  );
  
  return displayProducts.map(product => <ProductCard key={product.id} product={product} />);
}

Only use useMemo when the calculation is genuinely expensive (sorting/filtering thousands of items, complex computations). For simple array operations on small arrays, the useMemo overhead outweighs the benefit.


Technique 4: Code Splitting with React.lazy

import React, { Suspense, lazy } from 'react';

// Load AdminDashboard only when it's first rendered
const AdminDashboard = lazy(() => import('./AdminDashboard'));
const HeavyChart = lazy(() => import('./HeavyChart'));
const SettingsPanel = lazy(() => import('./SettingsPanel'));

function App() {
  const [view, setView] = useState<'home' | 'admin' | 'settings'>('home');
  
  return (
    <div>
      <nav>{/* navigation */}</nav>
      
      <Suspense fallback={<div className="loading">Loading...</div>}>
        {view === 'admin' && <AdminDashboard />}
        {view === 'settings' && <SettingsPanel />}
        {view === 'home' && <HeavyChart />}
      </Suspense>
    </div>
  );
}

The admin dashboard, chart library, and settings panel are only downloaded when the user navigates to them. Initial bundle size — and Time to Interactive — improves significantly.


Technique 5: Virtualize Long Lists

Rendering 1,000 DOM nodes is slow. Render only what's visible:

npm install react-window
import { FixedSizeList } from 'react-window';

function VirtualizedList({ items }: { items: Item[] }) {
  const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
    <div style={style} className="list-item">
      {items[index].name}
    </div>
  );
  
  return (
    <FixedSizeList
      height={600}        // Visible height of the list
      itemCount={items.length}
      itemSize={50}       // Height of each item in px
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}

For variable-height items, use VariableSizeList. For tables, consider react-virtuoso which handles both.


Technique 6: State Colocation

Move state down to where it's used. Unnecessary high-level state causes unnecessary re-renders:

// Bad: count state in parent causes entire parent to re-render
function ExpensiveParent() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      <VeryExpensiveComponent />  {/* Re-renders on every count change */}
    </div>
  );
}

// Good: extract counter into its own component
function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

function ExpensiveParent() {
  return (
    <div>
      <Counter />  {/* Counter state stays inside Counter */}
      <VeryExpensiveComponent />  {/* Never re-renders due to count */}
    </div>
  );
}

This pattern — colocating state — is often more impactful than adding memo or useMemo.


Performance Checklist

  • Profile first with React DevTools before optimizing
  • Use production build for accurate measurements (npm run build && npx serve dist)
  • Colocate state — don't lift state higher than necessary
  • Use React.memo for list items in large lists
  • Pair React.memo with useCallback for stable function props
  • Use useMemo only for genuinely expensive calculations
  • Lazy load large components not needed on initial render
  • Virtualize lists with more than 100–200 items
  • Analyze bundle size with npm run build -- --mode analyze

For the React fundamentals that these optimizations build on, see our React hooks tutorial. State management choices significantly impact performance — our React state management 2025 guide covers how Zustand and Jotai minimize unnecessary re-renders. And for the full React app architecture that influences performance, our React vs Next.js vs Remix guide covers rendering strategy decisions.


Frequently Asked Questions

When should I use React.memo?

List items in large lists, expensive components with stable props. Don't use it everywhere — measure first.

What causes unnecessary re-renders?

Parent re-renders, new object/array/function references on each render, context changes.

What is React.lazy?

Code splitting — components load as separate chunks only when first rendered. Reduces initial bundle size and improves Time to Interactive.

What is list virtualization?

Rendering only visible list items. Required for lists with 100–200+ items to maintain scroll performance.

Share this article:

Frequently Asked Questions

Use React.memo when a component renders the same output given the same props and re-renders frequently with unchanged props. Common cases: list items in a large list where only some items change, expensive-to-render components that receive stable props from the parent. Don't use it everywhere — memo has its own overhead (the props comparison), and the React compiler (coming in React 19) will handle memoization automatically.
A

AiTechWorlds Team

✓ Verified Writer

The AiTechWorlds team is passionate about AI, technology, and education. We create high-quality, research-backed content to help you learn, grow, and succeed in the modern digital world.

Related Articles

10K+ Members Growing Daily

Get Free AI Notes Daily

Join AiTechWorlds on Telegram and get daily AI tips, prompt engineering templates, coding resources, and exclusive content — 100% free!

📚 Free Study Notes🤖 AI Tips Daily⚡ Prompt Templates💻 Coding Resources
Join Free Channel

No spam. Leave anytime.

!