React Performance Optimization

Techniques to make your React apps faster

⚡ What is Performance Optimization?

Performance optimization involves techniques to make React apps faster and more efficient. This includes reducing unnecessary renders, optimizing bundle size, lazy loading components, and using memoization to prevent expensive recalculations.


// Optimize with React.memo
const ExpensiveComponent = React.memo(function({ data }) {
  return <div>{data}</div>;
});
                                    

Key Optimization Techniques

🎯

Memoization

Cache expensive calculations

🔄

Prevent Renders

Skip unnecessary re-renders

📦

Code Splitting

Load code on demand

Lazy Loading

Defer component loading

🔹 React.memo for Component Memoization

React.memo prevents re-renders when props haven't changed. Wrap components that render often with the same props to skip unnecessary renders and improve performance significantly.

import React from 'react';

// ❌ Without memo - re-renders every time parent renders
function ExpensiveList({ items }) {
  console.log('Rendering list...');
  return (
    <ul>
      {items.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
}

// ✅ With memo - only re-renders when items change
const OptimizedList = React.memo(function ExpensiveList({ items }) {
  console.log('Rendering list...');
  return (
    <ul>
      {items.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
});

Result:

Component only re-renders when items prop actually changes.

🔹 useMemo for Expensive Calculations

Use useMemo to cache expensive calculations between renders. The calculation only runs when dependencies change, preventing unnecessary work on every render.

import { useMemo, useState } from 'react';

function DataAnalysis({ data }) {
  const [filter, setFilter] = useState('');

  // ✅ Expensive calculation cached
  const processedData = useMemo(() => {
    console.log('Processing data...');
    return data
      .filter(item => item.name.includes(filter))
      .map(item => ({
        ...item,
        score: calculateComplexScore(item)
      }));
  }, [data, filter]); // Only recalculate when these change

  return (
    <div>
      <input 
        value={filter} 
        onChange={e => setFilter(e.target.value)} 
      />
      <DataTable data={processedData} />
    </div>
  );
}

🔹 useCallback for Function Memoization

useCallback prevents creating new function instances on every render. Essential when passing callbacks to memoized child components to prevent breaking their memoization.

import { useCallback, useState } from 'react';

function TodoList() {
  const [todos, setTodos] = useState([]);

  // ✅ Function reference stays the same
  const handleAddTodo = useCallback((text) => {
    setTodos(prev => [...prev, { id: Date.now(), text }]);
  }, []); // No dependencies - function never changes

  const handleDeleteTodo = useCallback((id) => {
    setTodos(prev => prev.filter(todo => todo.id !== id));
  }, []); // No dependencies

  return (
    <div>
      <TodoInput onAdd={handleAddTodo} />
      <TodoItems items={todos} onDelete={handleDeleteTodo} />
    </div>
  );
}

🔹 Lazy Loading Components

Split your app into smaller bundles that load on demand. This dramatically reduces initial load time by only loading what's needed:

import { lazy, Suspense } from 'react';

// ✅ Lazy load heavy components
const Dashboard = lazy(() => import('./Dashboard'));
const Reports = lazy(() => import('./Reports'));
const Settings = lazy(() => import('./Settings'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/reports" element={<Reports />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

🔹 Virtualization for Long Lists

Render only visible items in long lists using virtualization libraries. This keeps the DOM small even with thousands of items:

import { FixedSizeList } from 'react-window';

function VirtualizedList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      {items[index].name}
    </div>
  );

  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={50}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}

// Renders only ~12 items instead of 10,000!

🔹 Debouncing User Input

Delay expensive operations until user stops typing:

import { useState, useEffect } from 'react';

function SearchBox() {
  const [query, setQuery] = useState('');
  const [debouncedQuery, setDebouncedQuery] = useState('');

  // ✅ Debounce search
  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedQuery(query);
    }, 500); // Wait 500ms after typing stops

    return () => clearTimeout(timer);
  }, [query]);

  // Only search when debounced value changes
  useEffect(() => {
    if (debouncedQuery) {
      performExpensiveSearch(debouncedQuery);
    }
  }, [debouncedQuery]);

  return (
    <input 
      value={query} 
      onChange={e => setQuery(e.target.value)} 
    />
  );
}

🔹 Key Prop Optimization

Use stable keys for list items to help React identify changes:

// ❌ Bad - index as key
{items.map((item, index) => (
  <Item key={index} data={item} />
))}

// ✅ Good - stable unique ID
{items.map(item => (
  <Item key={item.id} data={item} />
))}

🔹 Best Practices

✅ Do:

  • Profile before optimizing (use React DevTools)
  • Use React.memo for expensive components
  • Lazy load routes and heavy components
  • Virtualize long lists (1000+ items)
  • Debounce expensive operations
  • Use production builds for testing

❌ Don't:

  • Optimize prematurely without measuring
  • Wrap every component in React.memo
  • Use index as key for dynamic lists
  • Forget to memoize callbacks passed to memoized components
  • Over-optimize small components

🧠 Test Your Knowledge

Which hook caches expensive calculations?