Course topics

By WebNest Studio Engineering Team

React Tutorial

Optimizing React Performance

React re-renders a component whenever its state changes, its parent re-renders, or the context it consumes changes. Most of the time this is fast enough to be invisible, but as component trees grow and lists get longer, unnecessary re-renders can start to cause visible lag. React provides specific tools to avoid renders that don't actually need to happen.

Performance work in React is mostly about doing less work, not doing the same work faster: skipping re-renders of components whose output wouldn't change, and giving React the information it needs to update lists efficiently.

Why it matters

React is fast by default, and most performance problems come from rendering far more than necessary: a whole list re-rendering for one changed item, or an expensive component re-rendering when its props have not changed. Knowing how to find and stop those renders, and when not to bother, is what the performance questions in interviews are about.

React.memo — Skipping Re-renders for Unchanged Props

By default, when a parent component re-renders, every child re-renders too, even if that child's props haven't changed. Wrapping a component in React.memo() makes React perform a shallow comparison of its props before re-rendering; if none of the props changed (by reference for objects/functions, by value for primitives), React skips re-rendering that component and reuses its last output. This is most valuable for components that are expensive to render or that re-render very often with the same props.

Stabilizing Props with useCallback and useMemo

React.memo only helps if the props passed to the memoized component actually stay the same reference between renders. Passing an inline function (onClick={() => doSomething()}) creates a brand-new function on every render, defeating memo. useCallback memoizes a function reference across renders (re-creating it only when its dependencies change), and useMemo memoizes a computed value the same way — both are the usual partners to React.memo.

Correct Keys for List Rendering

When rendering a list with .map(), React uses the key prop to match array items across renders, so it can update, reorder, add, or remove the corresponding DOM nodes efficiently instead of re-rendering the whole list. Keys must be stable and unique among siblings — a database id is ideal. Using the array index as a key works only if the list never reorders, is never filtered, and items are never inserted/removed from the middle; otherwise it can cause React to reuse the wrong DOM node and component state for the wrong item.

  • Use a stable unique id from your data (e.g. item.id) as the key, not the array index, whenever the list can reorder or change.
  • Never use Math.random() or a newly generated value as a key — that defeats the purpose of keys entirely, since it changes every render.
  • Keys only need to be unique among sibling elements in that one list, not globally unique across the whole app.

Example

React.memo combined with useCallback to prevent unnecessary child re-renders

JavaScript
import { useState, useCallback, memo } from 'react';

const ExpensiveListItem = memo(function ExpensiveListItem({ item, onSelect }) {
  console.log('Rendering item:', item.id);
  return <li onClick={() => onSelect(item.id)}>{item.name}</li>;
});

export default function ItemList() {
  const [items] = useState([
    { id: 1, name: 'Alpha' },
    { id: 2, name: 'Beta' },
    { id: 3, name: 'Gamma' },
  ]);
  const [counter, setCounter] = useState(0);

  // Stable function reference across renders — required for memo to help
  const handleSelect = useCallback((id) => {
    console.log('Selected item', id);
  }, []);

  return (
    <div>
      <button onClick={() => setCounter((c) => c + 1)}>
        Unrelated re-render trigger: {counter}
      </button>
      <ul>
        {items.map((item) => (
          <ExpensiveListItem key={item.id} item={item} onSelect={handleSelect} />
        ))}
      </ul>
    </div>
  );
}
Output
Clicking "Unrelated re-render trigger" increments the counter and re-renders ItemList, but because "items" never changes and handleSelect is memoized with useCallback, the memoized ExpensiveListItem components do not re-render — no new "Rendering item" console logs appear after the first render.

Common Mistakes

  • Wrapping every component in React.memo "just in case," which adds a comparison cost without a real re-render problem to solve.
  • Using React.memo on a component but still passing it a new inline object, array, or function prop each render, which defeats the memoization entirely.
  • Using the array index as the list key for a list that can be reordered, filtered, or have items inserted/removed, causing subtle state and UI bugs.
  • Reaching for useMemo/useCallback for trivial, cheap computations where the memoization overhead outweighs the benefit.

Exercise

Do this in src/App.jsx of a Vite + React project. ExpensiveList logs each time it renders. At present it re-renders whenever the unrelated counter changes. Wrap it in memo, and make sure the props it receives keep the same identity between renders.

Expected result: the console logs "list rendered" once on load, and clicking the counter button does not log it again.

JavaScript · starter code
import { useState } from "react";

function ExpensiveList({ items, onSelect }) {
  console.log("list rendered");
  return (
    <ul>
      {items.map((item) => (
        <li key={item} onClick={() => onSelect(item)}>{item}</li>
      ))}
    </ul>
  );
}

export default function App() {
  const [count, setCount] = useState(0);
  const [selected, setSelected] = useState(null);

  const items = ["pen", "bag", "book"];
  const handleSelect = (item) => setSelected(item);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count {count}</button>
      <p>Selected: {selected}</p>
      <ExpensiveList items={items} onSelect={handleSelect} />
    </div>
  );
}
Show hints
  • memo(Component) skips a re-render when every prop is the same as last time, compared by reference.
  • Move the constant array outside the component, and wrap the handler in useCallback.
Show solution
JavaScript · solution
import { memo, useCallback, useState } from "react";

const ITEMS = ["pen", "bag", "book"];

const ExpensiveList = memo(function ExpensiveList({ items, onSelect }) {
  console.log("list rendered");
  return (
    <ul>
      {items.map((item) => (
        <li key={item} onClick={() => onSelect(item)}>{item}</li>
      ))}
    </ul>
  );
});

export default function App() {
  const [count, setCount] = useState(0);
  const [selected, setSelected] = useState(null);

  const handleSelect = useCallback((item) => setSelected(item), []);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count {count}</button>
      <p>Selected: {selected}</p>
      <ExpensiveList items={ITEMS} onSelect={handleSelect} />
    </div>
  );
}

Quick quiz

1. What does React.memo do?

  • A. Caches API responses
  • B. Skips re-rendering a component when its props have not changed
  • C. Stores state permanently
  • D. Makes a component load lazily
Show answer

B. Props are compared shallowly, by reference.

2. By default, when a parent re-renders, what happens to its children?

  • A. They re-render too, whether or not their props changed
  • B. They never re-render
  • C. Only the first child re-renders
  • D. They are unmounted
Show answer

A. This is usually cheap; memo is for the cases where it is not.

3. What should be the first step in fixing a slow React screen?

  • A. Wrap every component in memo
  • B. Rewrite it in another framework
  • C. Remove all state
  • D. Measure with the React DevTools Profiler to find what is actually slow
Show answer

D. Optimising without measuring adds complexity and often misses the real cause.

Interview questions

How do you optimise the performance of a React application?

Show answer

Measure first with the React DevTools Profiler. Then address what it shows: prevent unnecessary re-renders with memo, stable props through useMemo and useCallback, and state kept close to where it is used. Render long lists with virtualisation, so that only visible rows exist. Split the code and load routes lazily with lazy and Suspense. Use correct keys, and debounce expensive work triggered by typing.

What causes a component to re-render?

Show answer

A change to its own state; a re-render of its parent, which re-renders all children by default; and a change to a context it reads. A change in props does not by itself trigger anything: props change because the parent re-rendered. Re-rendering means the component function runs again; React then compares the result with the previous one and updates the DOM only where they differ, so a re-render does not necessarily touch the DOM.

Key Points to Remember

  • React.memo skips re-rendering a component when its props are shallowly equal to the previous render.
  • useCallback and useMemo keep function and value references stable so memoized children actually benefit.
  • List keys should be stable, unique identifiers from the data, not array indexes, whenever the list can change order or membership.
  • Optimize based on measured, real re-render problems — premature memoization adds complexity without guaranteed benefit.

Practice in the playground

Write and run your own code for this topic, or start from the exercise or an example. Shows a live preview of plain HTML, CSS and JavaScript. JSX needs a React project on your computer. Your code for each topic is kept in this browser.

Run the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

These examples need a project environment on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.

WebNest Studio

Written by

WebNest Studio Engineering Team

Practical lessons from developers working with Java, Python, React, databases and production systems.

Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering