Performance Optimization

Memoization, code splitting, and React best practices

React Performance Basics

React is fast by default, but as your app grows, you might notice performance issues. Understanding how React renders and when to optimize is key to building fast applications.

The Golden Rule

Don't optimize prematurely. Measure first, optimize only when you have actual performance problems. Most React apps are fast enough without optimization.

Understanding Re-renders

React re-renders a component when:

// 1. State changes
function Counter() {
  const [count, setCount] = useState(0);
  // Every setCount call triggers a re-render
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

// 2. Props change
function Child({ name }) {
  // Re-renders when 'name' prop changes
  return <p>{name}</p>;
}

// 3. Parent re-renders (even if props don't change!)
function Parent() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      <Child name="John" />  {/* Re-renders even though name never changes! */}
    </div>
  );
}

// 4. Context changes
const ThemeContext = createContext();
function ThemedButton() {
  const theme = useContext(ThemeContext);
  // Re-renders when context value changes
  return <button className={theme}>Click</button>;
}

React.memo

Prevents re-renders when props haven't changed:

import { memo } from 'react';

// Without memo: re-renders whenever parent re-renders
function ExpensiveComponent({ data }) {
  // Expensive rendering logic...
  return <div>{data.name}</div>;
}

// With memo: only re-renders when props change
const MemoizedComponent = memo(function ExpensiveComponent({ data }) {
  return <div>{data.name}</div>;
});

// Custom comparison function
const MemoizedWithCompare = memo(
  function Component({ user }) {
    return <div>{user.name}</div>;
  },
  (prevProps, nextProps) => {
    // Return true if props are equal (skip re-render)
    return prevProps.user.id === nextProps.user.id;
  }
);

⚠️ memo Pitfall

memo only works if props are referentially stable. New objects/arrays/functions break memoization!

useMemo - Memoize Values

import { useMemo } from 'react';

function FilteredList({ items, filter }) {
  // Without useMemo: recalculates on every render
  // const filtered = items.filter(i => i.name.includes(filter));

  // With useMemo: only recalculates when items or filter change
  const filtered = useMemo(() => {
    console.log('Filtering...');
    return items.filter(i => i.name.includes(filter));
  }, [items, filter]);

  return (
    <ul>
      {filtered.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
}

// Expensive calculation
function Fibonacci({ n }) {
  const result = useMemo(() => {
    const fib = (num) => num <= 1 ? num : fib(num - 1) + fib(num - 2);
    return fib(n);
  }, [n]);

  return <p>Result: {result}</p>;
}

useCallback - Memoize Functions

import { useCallback, memo } from 'react';

// Child wrapped in memo
const Button = memo(function Button({ onClick, children }) {
  console.log('Button rendered');
  return <button onClick={onClick}>{children}</button>;
});

function Parent() {
  const [count, setCount] = useState(0);

  // Without useCallback: new function every render, breaks memo
  // const handleClick = () => console.log('clicked');

  // With useCallback: same function reference
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);

  // With dependencies
  const handleSubmit = useCallback(() => {
    console.log('Submitting count:', count);
  }, [count]);  // New function only when count changes

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      <Button onClick={handleClick}>Click me</Button>
    </div>
  );
}

Code Splitting with lazy()

import { lazy, Suspense } from 'react';

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

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

// Named exports
const Modal = lazy(() => 
  import('./components').then(module => ({ default: module.Modal }))
);

Virtualization for Long Lists

// npm install @tanstack/react-virtual

import { useVirtualizer } from '@tanstack/react-virtual';

function VirtualList({ items }) {
  const parentRef = useRef(null);

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,  // Estimated row height
  });

  return (
    <div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
      <div style={{ height: virtualizer.getTotalSize() }}>
        {virtualizer.getVirtualItems().map(virtualRow => (
          <div
            key={virtualRow.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: virtualRow.size,
              transform: `translateY(${virtualRow.start}px)`,
            }}
          >
            {items[virtualRow.index].name}
          </div>
        ))}
      </div>
    </div>
  );
}

// Only renders visible items, not all 10,000!

Debouncing and Throttling

import { useState, useMemo } from 'react';
import debounce from 'lodash/debounce';

function SearchInput() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  // Debounced search function
  const debouncedSearch = useMemo(
    () => debounce(async (searchQuery) => {
      const data = await fetch(`/api/search?q=${searchQuery}`);
      setResults(await data.json());
    }, 300),
    []
  );

  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>
  );
}

Measuring Performance

// React DevTools Profiler
// 1. Open React DevTools
// 2. Go to Profiler tab
// 3. Click Record, interact with app, stop
// 4. Analyze which components render and why

// Console timing
function ExpensiveComponent() {
  console.time('render');
  // ... component logic
  console.timeEnd('render');
  
  return <div>...</div>;
}

// React.Profiler component
import { Profiler } from 'react';

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

<Profiler id="Navigation" onRender={onRenderCallback}>
  <Navigation />
</Profiler>

Common Performance Issues

// ❌ Creating objects/arrays in render
function Bad() {
  return <Child style={{ color: 'red' }} />;  // New object every render!
}

// ✅ Move outside or memoize
const style = { color: 'red' };
function Good() {
  return <Child style={style} />;
}

// ❌ Inline function props
function Bad() {
  return <Button onClick={() => doSomething()} />;  // New function every render!
}

// ✅ Use useCallback
function Good() {
  const handleClick = useCallback(() => doSomething(), []);
  return <Button onClick={handleClick} />;
}

// ❌ State too high in tree
function App() {
  const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
  // Every mouse move re-renders entire app!
}

// ✅ Keep state close to where it's used
function MouseTracker() {
  const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
  // Only this component re-renders
}

🎯 Performance Best Practices

  • ✓ Measure before optimizing—use React DevTools Profiler
  • ✓ Keep state as local as possible
  • ✓ Use React.memo for expensive pure components
  • ✓ Use useMemo for expensive calculations
  • ✓ Use useCallback for stable function references
  • ✓ Lazy load routes and heavy components
  • ✓ Virtualize long lists (1000+ items)
  • ✓ Debounce/throttle frequent events
  • ✓ Avoid unnecessary re-renders from context