⚡ Performance Optimization

Building Fast & Efficient React Apps

Why Performance Matters

React is fast by default, but as apps grow, performance issues can arise. Learn techniques to optimize rendering, reduce bundle size, and improve user experience.

🎯 React.memo

Prevent unnecessary re-renders of components when props haven't changed.

// Without memo - re-renders on every parent update
function UserCard({ user }) {
  console.log('UserCard rendered');
  return <div>{user.name}</div>;
}

// With memo - only re-renders when user changes
const UserCard = React.memo(function UserCard({ user }) {
  console.log('UserCard rendered');
  return <div>{user.name}</div>;
});

function App() {
  const [count, setCount] = useState(0);
  const user = { name: 'John', id: 1 };
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <UserCard user={user} /> {/* Won't re-render when count changes */}
    </div>
  );
}

// Custom comparison
const UserCard = React.memo(UserCard, (prevProps, nextProps) => {
  return prevProps.user.id === nextProps.user.id;
});

🔧 useMemo Hook

Cache expensive computations between renders.

function ProductList({ products }) {
  const [filter, setFilter] = useState('');
  
  // Without useMemo - recalculates on every render
  const expensiveFiltered = products.filter(p => 
    p.name.toLowerCase().includes(filter.toLowerCase())
  ).sort((a, b) => b.price - a.price);
  
  // With useMemo - only recalculates when products or filter change
  const filteredProducts = useMemo(() => {
    console.log('Filtering products...');
    return products
      .filter(p => p.name.toLowerCase().includes(filter.toLowerCase()))
      .sort((a, b) => b.price - a.price);
  }, [products, filter]);
  
  return (
    <div>
      <input 
        value={filter} 
        onChange={(e) => setFilter(e.target.value)} 
      />
      {filteredProducts.map(p => (
        <div key={p.id}>{p.name} - ${p.price}</div>
      ))}
    </div>
  );
}

// Example: Expensive calculation
function DataTable({ data }) {
  const [page, setPage] = useState(1);
  
  const statistics = useMemo(() => {
    const total = data.reduce((sum, item) => sum + item.value, 0);
    const average = total / data.length;
    const max = Math.max(...data.map(d => d.value));
    return { total, average, max };
  }, [data]);  // Only recalculate when data changes
  
  return <div>Total: {statistics.total}</div>;
}

🎣 useCallback Hook

Memoize functions to prevent creating new references on each render.

function TodoList() {
  const [todos, setTodos] = useState([]);
  
  // Without useCallback - new function on every render
  const handleDelete = (id) => {
    setTodos(todos.filter(t => t.id !== id));
  };
  
  // With useCallback - same function reference
  const handleDelete = useCallback((id) => {
    setTodos(prev => prev.filter(t => t.id !== id));
  }, []);  // Empty deps - function never changes
  
  return (
    <div>
      {todos.map(todo => (
        <TodoItem 
          key={todo.id} 
          todo={todo} 
          onDelete={handleDelete}  // Same reference
        />
      ))}
    </div>
  );
}

const TodoItem = React.memo(({ todo, onDelete }) => {
  console.log('TodoItem rendered');
  return (
    <div>
      {todo.text}
      <button onClick={() => onDelete(todo.id)}>Delete</button>
    </div>
  );
});

📋 Code Splitting & Lazy Loading

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

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

function App() {
  const [page, setPage] = useState('dashboard');
  
  return (
    <div>
      <nav>
        <button onClick={() => setPage('dashboard')}>Dashboard</button>
        <button onClick={() => setPage('profile')}>Profile</button>
        <button onClick={() => setPage('settings')}>Settings</button>
      </nav>
      
      <Suspense fallback={<div>Loading...</div>}>
        {page === 'dashboard' && <Dashboard />}
        {page === 'profile' && <Profile />}
        {page === 'settings' && <Settings />}
      </Suspense>
    </div>
  );
}

// Route-based code splitting
import { BrowserRouter, Routes, Route } from 'react-router-dom';

const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<Spinner />}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/contact" element={<Contact />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

🔄 Virtual Scrolling

Render only visible items in large lists.

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

function LargeList({ 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>
  );
}

// Without virtual scrolling - renders 10,000 items
function SlowList({ items }) {
  return (
    <div>
      {items.map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
}

// With virtual scrolling - only renders ~15 visible items
function FastList({ items }) {
  return (
    <FixedSizeList
      height={600}
      itemCount={10000}
      itemSize={50}
    >
      {Row}
    </FixedSizeList>
  );
}

🎨 Debouncing & Throttling

// Debounce - wait for user to stop typing
function SearchBox() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  
  const debouncedSearch = useMemo(
    () => debounce(async (searchQuery) => {
      const data = await fetch(`/api/search?q=${searchQuery}`);
      setResults(await data.json());
    }, 500),
    []
  );
  
  const handleChange = (e) => {
    const value = e.target.value;
    setQuery(value);
    debouncedSearch(value);
  };
  
  return (
    <div>
      <input value={query} onChange={handleChange} />
      <ul>{results.map(r => <li key={r.id}>{r.name}</li>)}</ul>
    </div>
  );
}

function debounce(fn, delay) {
  let timeoutId;
  return (...args) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn(...args), delay);
  };
}

// Throttle - limit execution rate
function ScrollTracker() {
  const [scrollY, setScrollY] = useState(0);
  
  useEffect(() => {
    const handleScroll = throttle(() => {
      setScrollY(window.scrollY);
    }, 100);
    
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, []);
  
  return <div>Scroll position: {scrollY}</div>;
}

function throttle(fn, delay) {
  let lastCall = 0;
  return (...args) => {
    const now = Date.now();
    if (now - lastCall >= delay) {
      lastCall = now;
      fn(...args);
    }
  };
}

📦 Bundle Optimization

// Import only what you need
// ❌ Imports entire library
import _ from 'lodash';

// ✅ Import specific function
import debounce from 'lodash/debounce';

// Tree-shaking friendly imports
// ❌
import { Button, Modal, Dropdown } from 'huge-ui-library';

// ✅
import Button from 'huge-ui-library/Button';
import Modal from 'huge-ui-library/Modal';

// Dynamic imports
// ❌ Import at top level
import HeavyChart from './HeavyChart';

// ✅ Import when needed
function Dashboard() {
  const [showChart, setShowChart] = useState(false);
  const [Chart, setChart] = useState(null);
  
  useEffect(() => {
    if (showChart && !Chart) {
      import('./HeavyChart').then(module => {
        setChart(() => module.default);
      });
    }
  }, [showChart]);
  
  return (
    <div>
      <button onClick={() => setShowChart(true)}>Show Chart</button>
      {Chart && <Chart />}
    </div>
  );
}

🔍 React DevTools Profiler

// Use Profiler to measure performance
import { Profiler } from 'react';

function onRenderCallback(
  id,        // Component name
  phase,     // "mount" or "update"
  actualDuration,  // Time spent rendering
  baseDuration,    // Estimated time without memoization
  startTime,
  commitTime
) {
  console.log(`${id} took ${actualDuration}ms to render`);
}

function App() {
  return (
    <Profiler id="App" onRender={onRenderCallback}>
      <Dashboard />
      <UserList />
    </Profiler>
  );
}

// In browser DevTools:
// 1. Open React DevTools
// 2. Click Profiler tab
// 3. Click record button
// 4. Interact with app
// 5. Stop recording
// 6. Analyze flame graph

📊 Performance Checklist

Technique When to Use Impact
React.memo Pure components with same props High
useMemo Expensive calculations Medium
useCallback Functions passed to memoized children Medium
Code Splitting Large routes/components High
Virtual Scrolling Lists with 100+ items Very High
Debounce/Throttle Frequent events (scroll, input) High

🎯 Key Takeaways