React Tutorial 0/120 lessons ~6 min read Lesson 41

    Performance Optimization

    Hook performance optimization covers measuring before optimizing, useMemo, useCallback, React.memo, and avoiding premature memoization.

    Course progress0%
    Focus
    52 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    React applications can become slow when components perform expensive calculations, create unnecessary work, or cause avoidable re-renders.

    React Hooks provide several tools that can help optimize performance:

    • useMemo
    • useCallback
    • useRef
    • useReducer

    Performance optimization is not about using these Hooks everywhere.

    The goal is to identify expensive or unnecessary work and optimize it only when there is a meaningful benefit.

    What Makes a React Application Slow?

    Performance problems can come from several sources:

    • expensive calculations
    • unnecessary re-renders
    • large component trees
    • unstable object references
    • unstable function references
    • excessive Effects
    • unnecessary state updates
    • expensive list rendering
    • large amounts of DOM
    • slow external systems

    A useful mental model is:

    text
    State / Props Change
    ↓
    Component Render
    ↓
    Calculations
    ↓
    Child Components
    ↓
    DOM Commit

    Performance optimization tries to reduce unnecessary work in this pipeline.

    Rendering Is Not Automatically a Problem

    A React component rendering again does not necessarily mean there is a performance problem.

    React is designed to re-render components when their state or relevant inputs change.

    The important question is:

    Is the render doing expensive or unnecessary work?

    Do not optimize simply because you see a re-render.

    Measure first when possible.

    The Main Optimization Tools

    React provides several tools for different problems.

    ToolPrimary Purpose
    useMemoCache an expensive calculation
    useCallbackCache a function reference
    useRefHold mutable values without causing renders
    React.memoSkip child rendering when props are unchanged
    useReducerStructure complex state transitions

    These tools solve different problems.

    useMemo

    useMemo caches the result of a calculation.

    javascript
    const filteredProducts = useMemo(() => {
    return products.filter((product) =>
    product.name.includes(query)
    );
    }, [products, query]);

    React can reuse the previously calculated value when the dependencies have not changed.

    When useMemo Helps

    Consider:

    javascript
    const result = expensiveCalculation(data);

    If expensiveCalculation is costly and the component renders frequently for unrelated reasons, the calculation may be repeated unnecessarily.

    With:

    javascript
    const result = useMemo(() => {
    return expensiveCalculation(data);
    }, [data]);

    the result can be reused while data remains unchanged.

    useMemo Is a Performance Optimization

    Do not use useMemo as general-purpose storage.

    This:

    javascript
    const value = useMemo(() => {
    return calculateValue();
    }, []);

    does not mean:

    This value is permanently stored.

    It means React may reuse the memoized calculation result when appropriate.

    The dependency list determines when React needs to reconsider the calculation.

    Avoid Memoizing Cheap Calculations

    This is usually unnecessary:

    javascript
    const fullName = useMemo(() => {
    return `${firstName} ${lastName}`;
    }, [firstName, lastName]);

    The calculation is extremely cheap.

    Prefer:

    javascript
    const fullName = `${firstName} ${lastName}`;

    Memoization itself has complexity and overhead.

    Use it where it provides value.

    useCallback

    useCallback memoizes a function reference.

    javascript
    const handleDelete = useCallback((id) => {
    deleteItem(id);
    }, []);

    This can be useful when function identity matters.

    For example, when passing a callback to a memoized child:

    javascript
    const Child = React.memo(function Child({
    onDelete,
    }) {
    // ...
    });

    Then:

    javascript
    <Child onDelete={handleDelete} />

    can benefit from the stable callback reference.

    Function Identity

    Every time a component renders, a function created inside the component normally gets a new function identity.

    Example:

    javascript
    function Parent() {
    const handleClick = () => {
    console.log("Clicked");
    };
    return <Child onClick={handleClick} />;
    }

    Even if the function behaves the same, its reference is different between renders.

    Conceptually:

    text
    Render 1
    handleClick → Function A
    Render 2
    handleClick → Function B

    useCallback can preserve the reference:

    javascript
    const handleClick = useCallback(() => {
    console.log("Clicked");
    }, []);

    Conceptually:

    text
    Render 1
    handleClick → Function A
    Render 2
    handleClick → Function A

    as long as its dependencies remain unchanged.

    useCallback Does Not Make Functions Execute Faster

    This is an important distinction.

    useCallback does not optimize the internal execution speed of:

    javascript
    function handleClick() {
    expensiveOperation();
    }

    It helps preserve the function's identity.

    Use it when stable references matter.

    React.memo

    React.memo can skip rendering a child when its props are considered unchanged.

    Example:

    javascript
    const ProductItem = React.memo(function ProductItem({
    product,
    }) {
    return <li>{product.name}</li>;
    });

    If the parent renders again but the product prop remains equivalent according to the memo comparison, React can skip rendering that child.

    However, memoization is most effective when the props themselves remain stable.

    useCallback + React.memo

    These tools are often used together.

    Parent:

    javascript
    function TodoList() {
    const [todos, setTodos] = useState([]);
    const handleDelete = useCallback((id) => {
    setTodos((current) =>
    current.filter((todo) => todo.id !== id)
    );
    }, []);
    return (
    <TodoItems
    todos={todos}
    onDelete={handleDelete}
    />
    );
    }

    Memoized child:

    javascript
    const TodoItems = React.memo(function TodoItems({
    todos,
    onDelete,
    }) {
    // ...
    });

    The stable callback helps the memoized child avoid unnecessary prop changes caused only by a newly created function.

    Functional Updates and Stable Callbacks

    Functional state updates are often useful for creating callbacks with fewer dependencies.

    Instead of:

    javascript
    const addTodo = useCallback(() => {
    setTodos([
    ...todos,
    newTodo,
    ]);
    }, [todos]);

    you can use:

    javascript
    const addTodo = useCallback(() => {
    setTodos((current) => [
    ...current,
    newTodo,
    ]);
    }, [newTodo]);

    The callback no longer needs to read the current todos variable.

    This can simplify dependency management.

    useRef for Performance

    useRef can hold a mutable value without triggering a re-render when the value changes.

    javascript
    const renderCount = useRef(0);
    renderCount.current += 1;

    Changing:

    javascript
    renderCount.current

    does not cause React to render again.

    This makes refs useful for:

    • DOM references
    • timer IDs
    • previous values
    • external instances
    • mutable values that do not affect UI

    Do not use refs as a replacement for state when the value needs to appear in the UI.

    State vs Ref

    Use state when:

    text
    Value changes
    ↓
    UI should update

    Use a ref when:

    text
    Value changes
    ↓
    UI does not need to update

    Example state:

    javascript
    const [count, setCount] = useState(0);

    Example ref:

    javascript
    const timerId = useRef(null);

    Avoid Unnecessary State

    Unnecessary state can cause unnecessary renders.

    For example:

    javascript
    const [fullName, setFullName] = useState("");
    useEffect(() => {
    setFullName(`${firstName} ${lastName}`);
    }, [firstName, lastName]);

    Prefer:

    javascript
    const fullName = `${firstName} ${lastName}`;

    This removes:

    • a state variable
    • an Effect
    • a state update
    • an extra render caused by that update

    Simpler code is often faster code.

    Derived Data

    Derived values should normally be calculated from existing state and props.

    Example:

    javascript
    const completedTodos = todos.filter(
    (todo) => todo.completed
    );

    If this calculation is cheap, calculate it directly.

    If it is genuinely expensive:

    javascript
    const completedTodos = useMemo(() => {
    return todos.filter(
    (todo) => todo.completed
    );
    }, [todos]);

    The optimization should be justified by the cost of the calculation.

    Avoid Unnecessary Effect Chains

    Consider:

    text
    State A
    ↓
    Effect
    ↓
    State B
    ↓
    Effect
    ↓
    State C

    This can produce multiple updates and make the application difficult to reason about.

    Prefer deriving values:

    text
    State
    ↓
    Calculation
    ↓
    UI

    or use explicit event/state transitions when a real state change is required.

    Object Identity

    JavaScript objects are compared by reference.

    For example:

    javascript
    const userA = {
    name: "Asha",
    };
    const userB = {
    name: "Asha",
    };

    Even though the contents look identical:

    javascript
    userA === userB

    is:

    text
    false

    This matters for React optimization.

    If a parent creates a new object on every render:

    javascript
    <Profile
    user={{
    name: "Asha",
    }}
    />

    the child receives a new object reference each time.

    useMemo for Stable Object References

    Sometimes you need a stable object reference.

    For example:

    javascript
    const options = useMemo(() => {
    return {
    theme,
    compact,
    };
    }, [theme, compact]);

    Now options can remain referentially stable while its dependencies remain unchanged.

    This can matter when:

    • passing options to memoized children
    • using an object as a dependency
    • integrating with external libraries

    Do not memoize every object automatically.

    Context and Performance

    Context can cause consumers to re-render when the context value changes.

    Consider:

    javascript
    <AuthContext.Provider
    value={{
    user,
    login,
    logout,
    }}
    >
    {children}
    </AuthContext.Provider>

    The object passed to value may be recreated on each provider render.

    In some architectures, you may stabilize the value:

    javascript
    const value = useMemo(() => {
    return {
    user,
    login,
    logout,
    };
    }, [user, login, logout]);

    Functions such as login and logout may themselves use useCallback when stable references are useful.

    However, context performance should be evaluated based on actual component behavior rather than automatically memoizing everything.

    Large Lists

    Rendering large lists can become expensive.

    For example:

    javascript
    {products.map((product) => (
    <ProductCard
    key={product.id}
    product={product}
    />
    ))}

    Potential strategies include:

    • keeping list items simple
    • avoiding unnecessary state in parents
    • memoizing expensive item components when useful
    • keeping props stable when it matters
    • virtualization for very large lists

    For extremely large lists, virtualization renders only the visible portion rather than creating every item in the DOM at once.

    Stable Keys

    Good keys help React correctly identify list items.

    Prefer:

    javascript
    key={product.id}

    Avoid using array indexes when the list can be reordered, inserted into, or filtered:

    javascript
    key={index}

    Stable keys are primarily about correct reconciliation and state preservation, not simply performance.

    Expensive Calculations

    Consider:

    javascript
    function Dashboard({ transactions }) {
    const report = generateLargeFinancialReport(
    transactions
    );
    return <Report data={report} />;
    }

    If generating the report is expensive and the component frequently re-renders for unrelated reasons, memoization may help:

    javascript
    const report = useMemo(() => {
    return generateLargeFinancialReport(
    transactions
    );
    }, [transactions]);

    This is a reasonable use of useMemo because the calculation is potentially expensive.

    Performance Measurement

    Do not guess about performance.

    Measure it.

    Useful tools include:

    • React DevTools Profiler
    • browser Performance tools
    • application-specific timing
    • production performance monitoring

    Ask:

    text
    What is slow?
    ↓
    Why is it slow?
    ↓
    How often does it happen?
    ↓
    What optimization reduces the cost?
    ↓
    Did measurement improve?

    This is better than blindly adding memoization.

    React DevTools Profiler

    The React DevTools Profiler can help identify:

    • components that render frequently
    • expensive renders
    • render durations
    • components affected by updates

    A useful workflow is:

    text
    1. Reproduce the slow interaction.
    2. Record the interaction.
    3. Identify expensive components.
    4. Determine why they render.
    5. Optimize the actual bottleneck.
    6. Measure again.

    Performance Optimization Workflow

    A disciplined optimization process looks like this:

    Step 1: Identify the Problem

    Example:

    text
    Typing in search box feels slow.

    Step 2: Measure

    Determine whether the bottleneck is:

    • filtering
    • rendering
    • network activity
    • DOM size
    • unnecessary updates

    Step 3: Form a Hypothesis

    For example:

    text
    Filtering 50,000 products on every keystroke is expensive.

    Step 4: Optimize

    Possible solutions:

    javascript
    useMemo

    or:

    text
    debouncing
    virtualization
    better data structures

    depending on the actual bottleneck.

    Step 5: Measure Again

    Verify that the optimization helped.

    Memoization Has a Cost

    Memoization is not free.

    A memoized calculation needs:

    • dependency tracking
    • cached value management
    • additional code
    • memory
    • reasoning about dependencies

    Therefore:

    javascript
    useMemo(() => 2 + 2, []);

    is usually pointless.

    Simply write:

    javascript
    const value = 2 + 2;

    The optimization should be worth the complexity.

    Dependency Arrays and Performance

    Consider:

    javascript
    const result = useMemo(() => {
    return expensiveCalculation(data);
    }, [data]);

    If data is recreated unnecessarily:

    javascript
    const data = [...originalData];

    then the memoized calculation may run again.

    Memoization works best when its dependencies are themselves stable when appropriate.

    This is why performance optimization often involves the entire data flow, not just adding useMemo.

    Avoid Mutating Memoized Values

    Do not do:

    javascript
    const products = useMemo(() => {
    return createProducts();
    }, []);
    products.push(newProduct);

    The memoized value should be treated as immutable from the component's perspective.

    If you need a changed array, create a new one:

    javascript
    const updatedProducts = [
    ...products,
    newProduct,
    ];

    Performance and Immutability

    Immutability supports predictable rendering and comparison.

    For example:

    javascript
    setUser((current) => ({
    ...current,
    name: "Asha",
    }));

    creates a new object.

    This allows React and memoization strategies to distinguish changed references from unchanged references.

    Avoid mutating state directly:

    javascript
    user.name = "Asha";

    Avoid Creating Unnecessary Work During Render

    Consider:

    javascript
    function ProductList({ products }) {
    const data = products.map((product) => {
    return {
    ...product,
    formattedPrice: formatPrice(product.price),
    };
    });
    return <List data={data} />;
    }

    If this transformation is expensive and the component renders frequently for unrelated reasons, you may consider:

    javascript
    const data = useMemo(() => {
    return products.map((product) => ({
    ...product,
    formattedPrice: formatPrice(product.price),
    }));
    }, [products]);

    But first determine whether the transformation is actually expensive.

    Splitting Components

    Component boundaries can improve performance.

    Instead of one huge component:

    text
    Dashboard
    ├── Header
    ├── Sidebar
    ├── Chart
    ├── Table
    └── Activity

    extract meaningful components:

    javascript
    <Header />
    <Sidebar />
    <Chart />
    <TransactionTable />
    <ActivityFeed />

    This improves:

    • maintainability
    • readability
    • state ownership
    • optimization opportunities

    A smaller component can sometimes avoid work that an unrelated section does not need.

    Localize State

    State placement affects re-render scope.

    Suppose a modal only affects one section.

    Keeping its state near the modal can avoid causing an entire application subtree to render unnecessarily.

    Prefer:

    text
    Modal
    └── modal state

    instead of unnecessarily storing:

    text
    App
    └── modal state

    when many unrelated components depend on the parent.

    A useful rule is:

    Keep state as close as practical to the components that need it.

    Avoid Lifting State Too Far

    State should be lifted when multiple components need it.

    But lifting state too high can cause a larger subtree to re-render.

    For example:

    text
    App
    ├── Header
    ├── Dashboard
    ├── Sidebar
    └── Footer

    If only Dashboard needs rapidly changing state, placing that state in App may cause unrelated sections to participate in updates.

    Prefer local state when sharing is unnecessary.

    useReducer and Performance

    useReducer is primarily a state-organization tool, not a direct performance optimization.

    It can still help structure complex state updates.

    For example:

    javascript
    const [state, dispatch] = useReducer(
    reducer,
    initialState
    );

    The benefit is clearer transitions:

    javascript
    dispatch({
    type: "filterChanged",
    payload: "completed",
    });

    Do not choose useReducer simply because you think it is faster than useState.

    Choose it when the state model benefits from reducer-based transitions.

    Custom Hooks and Performance

    A Custom Hook can encapsulate optimization logic.

    For example:

    javascript
    function useFilteredProducts(
    products,
    query
    ) {
    return useMemo(() => {
    return products.filter((product) =>
    product.name
    .toLowerCase()
    .includes(query.toLowerCase())
    );
    }, [products, query]);
    }

    Now the optimization is reusable:

    javascript
    const filteredProducts =
    useFilteredProducts(products, query);

    Custom Hooks should still avoid unnecessary memoization.

    Debouncing vs Memoization

    These solve different problems.

    Memoization

    Prevents repeating a calculation when inputs have not changed.

    javascript
    useMemo(...)

    Debouncing

    Delays work until input changes stop for a specified period.

    For example:

    text
    User types
    ↓
    R
    ↓
    Re
    ↓
    Rea
    ↓
    React
    ↓
    wait
    ↓
    perform search

    Debouncing is useful for:

    • search
    • autocomplete
    • filtering
    • validation
    • resize-related work

    Memoization and debouncing are not interchangeable.

    Memoization vs Virtualization

    These are also different.

    Memoization

    Reduces repeated calculations or renders.

    Virtualization

    Reduces the number of rendered DOM elements.

    For a list with:

    text
    100,000 items

    memoizing each item does not necessarily solve the cost of having 100,000 DOM nodes.

    Virtualization may be more appropriate.

    Lazy Initialization

    State initialization can itself be expensive.

    Instead of:

    javascript
    const [data, setData] = useState(
    expensiveInitialization()
    );

    the function may be evaluated before useState receives its argument.

    For expensive initialization, use a lazy initializer:

    javascript
    const [data, setData] = useState(() =>
    expensiveInitialization()
    );

    This allows React to initialize the state using the function rather than evaluating the expensive call on every component invocation.

    useReducer Lazy Initialization

    useReducer also supports lazy initialization:

    javascript
    const [state, dispatch] = useReducer(
    reducer,
    initialArgument,
    init
    );

    Example:

    javascript
    function init(initialCount) {
    return {
    count: initialCount,
    };
    }

    Then:

    javascript
    const [state, dispatch] = useReducer(
    reducer,
    100,
    init
    );

    This is useful when initial state construction is expensive or needs dedicated initialization logic.

    Common Performance Mistakes

    1. Memoizing Everything

    Avoid:

    javascript
    useMemo(...)
    useCallback(...)

    everywhere without evidence of a problem.

    2. Using useMemo for Cheap Values

    Do not optimize trivial calculations.

    3. Using useCallback Everywhere

    Stable function references are useful only when they matter.

    4. Treating React.memo as a Guarantee

    React.memo is an optimization, not a semantic requirement that a component will never render.

    5. Storing Derived Values in State

    This can introduce unnecessary renders and synchronization complexity.

    6. Lifting State Too High

    Unnecessary parent state can expand the scope of re-renders.

    7. Ignoring Large Lists

    Rendering thousands of DOM nodes can be expensive even when individual components are simple.

    8. Ignoring Effects

    Poorly designed Effects can cause:

    • repeated requests
    • subscription leaks
    • update loops
    • unnecessary state changes

    9. Optimizing Without Measuring

    The fastest-looking code is not always the fastest code.

    Measure the actual bottleneck.

    Best Practices

    1. Measure Before Optimizing

    Use profiling and browser performance tools.

    2. Optimize Expensive Work

    Focus on actual bottlenecks.

    3. Keep State Local

    Avoid unnecessarily broad re-render scopes.

    4. Derive Values Instead of Storing Them

    Prefer calculations during rendering when they are cheap.

    5. Use useMemo for Expensive Calculations

    Do not use it automatically.

    6. Use useCallback When Function Identity Matters

    Especially around memoized children or dependency relationships.

    7. Use React.memo Selectively

    Memoize components when it meaningfully reduces rendering work.

    8. Keep Props Stable When Necessary

    Stable references can make memoization effective.

    9. Keep Effects Correct

    Incorrect Effects can create much larger performance problems than missing memoization.

    10. Consider List Virtualization

    For very large lists, reducing DOM size can be more valuable than memoizing calculations.

    11. Prefer Simpler Code

    An optimization that makes the code significantly harder to understand should have a measurable benefit.

    Practical Example: Optimized Product List

    javascript
    import {
    memo,
    useCallback,
    useMemo,
    useState,
    } from "react";
    const ProductItem = memo(function ProductItem({
    product,
    onSelect,
    }) {
    return (
    <li>
    <span>{product.name}</span>
    <button
    onClick={() => onSelect(product.id)}
    >
    Select
    </button>
    </li>
    );
    });
    function ProductList({ products }) {
    const [query, setQuery] = useState("");
    const [selectedId, setSelectedId] =
    useState(null);
    const filteredProducts = useMemo(() => {
    const normalizedQuery =
    query.toLowerCase();
    return products.filter((product) =>
    product.name
    .toLowerCase()
    .includes(normalizedQuery)
    );
    }, [products, query]);
    const handleSelect = useCallback((id) => {
    setSelectedId(id);
    }, []);
    return (
    <div>
    <input
    value={query}
    onChange={(event) =>
    setQuery(event.target.value)
    }
    placeholder="Search products"
    />
    <ul>
    {filteredProducts.map((product) => (
    <ProductItem
    key={product.id}
    product={product}
    onSelect={handleSelect}
    />
    ))}
    </ul>
    {selectedId !== null && (
    <p>Selected: {selectedId}</p>
    )}
    </div>
    );
    }
    export default ProductList;

    This example combines:

    • useMemo for filtering
    • useCallback for stable function identity
    • memo for child rendering optimization
    • local state
    • stable list keys

    The important point is that each optimization has a specific reason.

    Practical Example: Avoiding Unnecessary State

    Before:

    javascript
    function CartSummary({ items }) {
    const [total, setTotal] = useState(0);
    useEffect(() => {
    setTotal(
    items.reduce(
    (sum, item) =>
    sum + item.price * item.quantity,
    0
    )
    );
    }, [items]);
    return <p>Total: ₹{total}</p>;
    }

    After:

    javascript
    function CartSummary({ items }) {
    const total = items.reduce(
    (sum, item) =>
    sum + item.price * item.quantity,
    0
    );
    return <p>Total: ₹{total}</p>;
    }

    If the calculation is inexpensive, the second version is better.

    If the calculation becomes genuinely expensive:

    javascript
    const total = useMemo(() => {
    return items.reduce(
    (sum, item) =>
    sum + item.price * item.quantity,
    0
    );
    }, [items]);

    Practical Example: Stable Context Value

    A provider might contain:

    javascript
    function AppProvider({ children }) {
    const [user, setUser] = useState(null);
    const login = useCallback((userData) => {
    setUser(userData);
    }, []);
    const logout = useCallback(() => {
    setUser(null);
    }, []);
    const value = useMemo(() => {
    return {
    user,
    login,
    logout,
    };
    }, [user, login, logout]);
    return (
    <AppContext.Provider value={value}>
    {children}
    </AppContext.Provider>
    );
    }

    This can reduce unnecessary context value identity changes.

    However, if profiling shows no meaningful issue, the simpler implementation may be preferable.

    Performance Optimization Checklist

    When a React component feels slow:

    Rendering

    • Is the component rendering more often than necessary?
    • Is the rendered tree unnecessarily large?
    • Can state be moved closer to where it is used?

    Calculations

    • Is an expensive calculation repeated?
    • Can useMemo help?

    Functions

    • Are callback references causing memoized children to receive changed props?
    • Would useCallback help?

    Components

    • Would React.memo meaningfully reduce rendering?

    Effects

    • Is an Effect causing unnecessary state updates?
    • Are requests or subscriptions being repeated?
    • Is cleanup correct?

    Lists

    • Is the list extremely large?
    • Would virtualization help?

    Data

    • Are objects and arrays being recreated unnecessarily?
    • Are references stable where they need to be?

    Measurement

    • Have you profiled the actual bottleneck?

    Performance Optimization Strategy

    A good optimization strategy can be summarized as:

    text
    Keep the architecture simple
    ↓
    Measure performance
    ↓
    Identify bottleneck
    ↓
    Optimize specific work
    ↓
    Measure again
    ↓
    Keep only useful optimization

    Avoid:

    text
    Add useMemo everywhere
    Add useCallback everywhere
    Add React.memo everywhere
    Hope it gets faster

    Performance engineering should be evidence-driven.

    Practice Exercises

    Exercise 1: useMemo

    Create a component that filters 10,000 products.

    Use useMemo to avoid repeating the filtering calculation when unrelated state changes.

    Exercise 2: useCallback

    Create a parent component and memoized child.

    Pass a callback from the parent to the child.

    Observe how function identity changes between renders and then use useCallback to stabilize it.

    Exercise 3: React.memo

    Create a list of products.

    Memoize the product item component and determine which updates cause the item to render.

    Exercise 4: Derived State

    Find a component that stores a value that can be calculated from existing state.

    Remove the unnecessary state and Effect.

    Exercise 5: State Localization

    Create a dashboard with:

    • counter
    • search box
    • sidebar
    • footer

    Keep the rapidly changing state as local as practical.

    Observe the effect of moving state higher or lower in the tree.

    Exercise 6: Large List

    Render a list containing thousands of items.

    Compare:

    • normal rendering
    • memoized items
    • virtualized rendering

    Identify which technique has the greatest impact.

    Exercise 7: Performance Profiling

    Use React DevTools Profiler to identify:

    • a frequently rendered component
    • an expensive calculation
    • an unnecessary state update

    Document what caused the performance problem and what optimization improved it.

    Mini Project: Optimized Product Dashboard

    Build a Product Dashboard containing:

    text
    Search
    Filters
    Product List
    Product Details
    Cart Summary

    Requirements:

    Search

    • search products by name
    • avoid unnecessary expensive filtering
    • debounce if searching an external API

    Filters

    Support:

    text
    Category
    Price range
    Availability

    Product List

    • use stable keys
    • keep item components focused
    • consider memoization when justified

    Product Details

    Only update the relevant section when possible.

    Cart Summary

    Derive totals instead of storing redundant state.

    Performance

    Profile the application and identify at least one actual bottleneck before adding an optimization.

    Possible architecture:

    text
    ProductDashboard
    │
    ├── Search
    │
    ├── Filters
    │
    ├── ProductList
    │ └── ProductItem
    │
    ├── ProductDetails
    │
    └── CartSummary

    Interview questions & answers

    Interview preparation

    12 questions grouped by difficulty — expand any card for a model answer and follow-up probe.

    Beginner

    4
    1. 1What is performance optimization in React?
      Beginner

      Model answer

      It is the process of reducing unnecessary rendering, calculations, DOM work, and external operations so the application responds efficiently.

    2. 2Does useCallback make a function faster?
      Beginner

      Model answer

      No.

      It primarily helps with function identity and can prevent unnecessary work in consumers that care about reference equality.

    3. 3When should you use useMemo?
      Beginner

      Model answer

      Use it when:
      • a calculation is expensive
      • the component renders often
      • the dependencies often remain unchanged
      • profiling or reasoning indicates the optimization is useful
    4. 4Is useReducer a performance Hook?
      Beginner

      Model answer

      Not primarily. useReducer is mainly for structuring complex state transitions. It may contribute indirectly to clearer state architecture, but it should not be chosen merely for performance.

    Intermediate

    4
    1. 5What does useMemo do?
      Intermediate

      Model answer

      useMemo caches the result of a calculation and can reuse it when its dependencies have not changed.
    2. 6What is React.memo?
      Intermediate

      Model answer

      React.memo is a component optimization that can skip re-rendering a component when its props are considered unchanged.
    3. 7When should you use useCallback?
      Intermediate

      Model answer

      Use it when function identity matters, such as when passing callbacks to memoized children or when a stable function reference is useful for another dependency relationship.

    4. 8How can state placement affect performance?
      Intermediate

      Model answer

      State updates can cause a component and its descendants to render.

      Keeping state closer to the components that need it can reduce the scope of updates.

    Advanced

    4
    1. 9What does useCallback do?
      Advanced

      Model answer

      useCallback caches a function reference and can preserve its identity between renders while its dependencies remain unchanged.
    2. 10Should every component use React.memo?
      Advanced

      Model answer

      No.

      Use it when it provides a measurable or meaningful benefit.

    3. 11What is the difference between useMemo and useCallback?
      Advanced

      Model answer

      useMemo → memoizes a value → useCallback → memoizes a function reference Conceptually: useCallback(fn, deps) is closely related to: useMemo(() => fn, deps) but useCallback communicates the intent more clearly.
    4. 12Why should you measure before optimizing?
      Advanced

      Model answer

      Because optimization adds complexity and a change that seems helpful may have little or no benefit.

      Profiling identifies the actual bottleneck.

    Performance Cheat Sheet

    Memoize expensive calculation

    javascript
    const result = useMemo(() => {
    return expensiveCalculation(data);
    }, [data]);

    Memoize callback

    javascript
    const handleClick = useCallback(() => {
    doSomething();
    }, []);

    Memoized component

    javascript
    const Child = memo(function Child(props) {
    return <div />;
    });

    Keep state local

    text
    State
    ↓
    Nearest practical owner
    ↓
    Smaller update scope

    Derive cheap values

    javascript
    const total = price * quantity;

    Functional state update

    javascript
    setCount((current) => current + 1);

    Large lists

    text
    Small list
    ↓
    Normal rendering
    Large list
    ↓
    Consider memoization
    Very large list
    ↓
    Consider virtualization

    Optimization workflow

    text
    Measure
    ↓
    Identify
    ↓
    Optimize
    ↓
    Measure again

    Key Takeaways

    Ready to mark this lesson complete?Track your journey across the entire course.