Skip to content

Memoization and Optimization — Complete Guide

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Memoization and Optimization. We cover key concepts, practical examples, and best practices to help you master this topic.

Memoization in Virtual Dom caches expensive computations and component outputs, preventing unnecessary re-renders and improving application performance significantly.

What You'll Learn

  • What memoization is and how it applies to Virtual DOM
  • How React.memo, useMemo, and useCallback work
  • How to identify and fix unnecessary re-renders
  • Advanced optimization patterns for large applications

Why It Matters

Without memoization, every parent re-render causes all children to re-render. For deep component trees with expensive computations, this creates unnecessary work that slows down the application.

flowchart LR
  A[Parent re-renders] --> B{Child memoized?}
  B -->|Yes| C{Props changed?}
  B -->|No| D[Child re-renders]
  C -->|Yes| D
  C -->|No| E[Child skips re-render]
  D --> F[Virtual DOM diffed]
  E --> G[Subtree bailed out]

What Memoization Does

Memoization caches results based on input dependencies.

// Without memoization: every render recomputes
function ExpensiveComponent({ items, filter }) {
    // This runs on EVERY render of the parent
    const filtered = items.filter(item => {
        // Expensive operation
        return item.name.includes(filter) &&
               complexCheck(item.data);
    });

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

// With memoization: only recomputes when dependencies change
function ExpensiveComponent({ items, filter }) {
    const filtered = useMemo(() => {
        // Only runs when items or filter change
        return items.filter(item => {
            return item.name.includes(filter) &&
                   complexCheck(item.data);
        });
    }, [items, filter]);  // Dependencies

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

// Without useMemo: O(n) filtering every render
// With useMemo: O(n) filtering only when items/filter change

React.memo for Components

React.memo prevents component re-rendering when props haven't changed.

// Unoptimized: re-renders on every parent render
function Child({ name, onClick }) {
    console.log('Child rendered:', name);
    return <button onClick={onClick}>{name}</button>;
}

// Optimized: only re-renders when props change (shallow comparison)
const MemoizedChild = React.memo(function Child({ name, onClick }) {
    console.log('Child rendered:', name);
    return <button onClick={onClick}>{name}</button>;
});

// Parent component
function Parent() {
    const [count, setCount] = useState(0);
    const [items, setItems] = useState(['A', 'B', 'C']);

    // Problem: new function reference every render
    // Even with React.memo, the child re-renders because onClick changes
    const handleClick = () => {
        console.log('Clicked');
    };

    // Solution: stable reference with useCallback
    const stableHandleClick = useCallback(() => {
        console.log('Clicked');
    }, []);  // No dependencies — stable for entire lifecycle

    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={() => setCount(c => c + 1)}>Increment</button>
            <MemoizedChild name="Stable" onClick={stableHandleClick} />
            {/* Child does NOT re-render when count changes */}
        </div>
    );
}

Identifying Unnecessary Re-Renders

Use tools and techniques to find optimization opportunities.

// 1. React DevTools: Highlight updates
// Enable "Highlight updates when components render" in React DevTools
// Green flashes indicate re-renders. Look for unnecessary flashes.

// 2. Custom logging hook
function useWhyDidYouUpdate(name, props) {
    const previousProps = useRef();

    useEffect(() => {
        if (previousProps.current) {
            const allKeys = Object.keys({ ...previousProps.current, ...props });
            const changedProps = {};

            allKeys.forEach(key => {
                if (previousProps.current[key] !== props[key]) {
                    changedProps[key] = {
                        from: previousProps.current[key],
                        to: props[key]
                    };
                }
            });

            if (Object.keys(changedProps).length > 0) {
                console.log('Component', name, 're-rendered because:', changedProps);
            }
        }

        previousProps.current = props;
    });
}

// Usage:
function MyComponent(props) {
    useWhyDidYouUpdate('MyComponent', props);
    return <div>{props.value}</div>;
}

// 3. Performance profiling
// Use React DevTools Profiler to record a session
// Look for components that rendered without prop changes
// These are candidates for memoization

Advanced Memoization Patterns

Beyond basic useMemo and useCallback.

// Pattern 1: Context splitting
// Instead of one large context, split by update frequency
const ThemeContext = createContext('light');
const UserContext = createContext(null);
const NotificationContext = createContext([]);

// Components that only need theme don't re-render
// when notifications change.
// Each context consumer only re-renders when its context changes.

// Pattern 2: Component lifting
// Move the stateful part above the memoized part
function Parent() {
    return (
        <ExpensiveList>
            <Counter />  {/* Counter re-renders independently */}
        </ExpensiveList>
    );
}

const ExpensiveList = React.memo(function({ children }) {
    return <div>{children}</div>;
});
// ExpensiveList doesn't re-render when Counter changes.

// Pattern 3: Selector functions
// Extract specific values from large objects
function DataView({ data }) {
    // Only re-render when specific fields change
    const title = useMemo(() => data.title, [data.title]);
    const items = useMemo(() => data.items, [data.items]);

    return (
        <div>
            <h1>{title}</h1>
            <List items={items} />
        </div>
    );
}

// Pattern 4: Stable references for callbacks
const handlers = useRef({
    onSave: (data) => saveData(data),
    onDelete: (id) => deleteItem(id)
});
// Never causes child re-renders due to callback changes

Profiling-Guided Optimization

Always measure before optimizing.

// 1. Add performance marks to identify slow components
function ProfiledComponent() {
    const startTime = useRef(performance.now());
    const renderCount = useRef(0);

    renderCount.current += 1;

    useEffect(() => {
        const elapsed = performance.now() - startTime.current;
        if (elapsed > 5) {
            console.warn('Slow render:', elapsed.toFixed(2) + 'ms', 'renders:', renderCount.current);
        }
        startTime.current = performance.now();
    });

    // Component content
    return <ExpensiveContent />;
}

// 2. Use requestAnimationFrame for animation performance
function AnimationFrameLogger() {
    const frameCount = useRef(0);

    useEffect(() => {
        let isActive = true;
        function logFPS() {
            if (!isActive) return;
            frameCount.current += 1;
            requestAnimationFrame(logFPS);
        }
        requestAnimationFrame(logFPS);

        const interval = setInterval(() => {
            console.log('FPS:', frameCount.current);
            frameCount.current = 0;
        }, 1000);

        return () => {
            isActive = false;
            clearInterval(interval);
        };
    }, []);
}

// 3. Only memoize when there's a measurable benefit
// Premature memoization adds code complexity without benefit

Common Mistakes

  1. Memoizing everything without measuring first (premature optimization).
  2. Using useMemo for primitive values (numbers, strings) that are cheap to compute.
  3. Forgetting dependencies in useMemo/useCallback, causing stale values.
  4. Using inline objects/arrays in JSX that break React.memo's shallow comparison.
  5. Overusing useCallback when the child component isn't memoized with React.memo.

Practice Questions

  1. What does React.memo do? It prevents a component from re-rendering if its props haven't changed (shallow comparison).
  2. What is the difference between useMemo and useCallback? useMemo caches a computed value. useCallback caches a function reference.
  3. When should you use useMemo? When a computation is expensive and its dependencies change infrequently.
  4. What breaks React.memo? Inline objects, arrays, and functions that create new references every render.

Challenge

Build a component tree with 50 nested components where each component renders a random number. Use React DevTools Profiler to identify unnecessary re-renders. Apply React.memo, useMemo, and useCallback to eliminate all unnecessary re-renders. Show the before and after comparison.

FAQ

What is memoization in React?

Memoization caches the result of a computation or component render so it doesn't need to be recomputed when inputs haven't changed.

Does React.memo do a deep comparison?

No. React.memo does a shallow comparison of props. For deep comparison, provide a custom comparison function as the second argument.

When should I NOT use React.memo?

When the component is cheap to render, when props always change, or when the component has few children.

Does useMemo prevent re-rendering?

No. useMemo only prevents recomputation of a value. It does not prevent the component from re-rendering.

What is the biggest cause of unnecessary re-renders?

New object/array/function references every render, which break shallow comparison in React.memo and PureComponent.

Mini Project

Build a React optimization dashboard. Create a complex component with multiple levels of nesting, expensive computations, and frequently updating state. Show a live display of which components are re-rendering and why. Apply optimizations step by step and show the reduction in render count and render time.

What's Next

Lesson 14: Virtual DOM and SSR

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro