Skip to solution
hardFrontend

When does `React.memo` do nothing — or actually hurt performance?

404 views
01

Understand the problem

Question presented to candidate: "A colleague has wrapped most components in React.memo and the profiler looks the same as before. Why?"

What a strong answer should cover:

  • memo compares props by reference (a shallow equality check). Any prop that is a new object, array, or function each render fails that check every time.
  • So the classic no-op is an inline style={{...}}, items={[...]}, or onClick={() => ...} in the parent's JSX — the comparison is guaranteed to fail.
  • When the comparison always fails, memo is pure overhead: you pay the check and still render.
  • memo also does nothing if the component re-renders for a different reason — its own state changed, or a context it consumes changed. It only blocks re-renders caused by the parent.
  • Wrapping cheap components is a net loss: the comparison plus the extra memory can exceed the render it saves.
  • A custom comparator can make it worse — deep-comparing a large object every render, or silently going stale if you forget a prop.
  • The reliable fixes are stable references (hoist constants, useCallback, useMemo) or not passing the prop at all (composition via children).

Clarifying questions expected:

  • "What props does this component receive, and are any of them created inline?"
  • "Is the parent re-rendering, or is it this component's own state or context?"

Code / implementation expected: Optional. Showing the inline-prop failure and its hoisted fix is the clearest form.

reactperformancememoization
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for senior React interviews — assumes what React.memo is. Difficulty: Hard

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

The same memo component with an inline prop and a hoisted one
Run Playground
import { useState, useRef, useEffect, useCallback, memo } from "react";

// Hoisted to module scope: ONE object for the life of the program, so the
// memo comparison can actually succeed.
const STABLE_STYLE = { padding: "2px 6px", background: "#eef" };

const Child = memo(function Child({ label, style, onRendered }) {
  // Counted in an effect, not during render — an effect with no dependency
  // array runs after every commit of THIS component, and a memoised component
  // that skips its render skips its effects too. onRendered writes to a ref on
  // the parent, so counting never schedules another render.
  useEffect(() => { onRendered(label); });

  return (
    <div style={style}>
      <code>{label}</code>
    </div>
  );
});

export default function App() {
  const [n, setN] = useState(0);
  const counts = useRef({});

  // Stable identity, so passing it down does not itself break the comparison.
  const onRendered = useCallback((label) => {
    counts.current[label] = (counts.current[label] || 0) + 1;
  }, []);

  const rows = ["inline props", "stable style only", "all stable"];

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.8, maxWidth: 560 }}>
      <button onClick={() => setN((v) => v + 1)}>
        re-render the parent ({n})
      </button>

      <div style={{ marginTop: 14, display: "grid", gap: 8 }}>
        {/* ❌ inline object created fresh on every parent render */}
        <Child
          label="inline props"
          style={{ padding: "2px 6px", background: "#fee" }}
          onRendered={onRendered}
        />

        {/* ❌ stable style, but a brand new inline arrow every render */}
        <Child
          label="stable style only"
          style={STABLE_STYLE}
          onRendered={(l) => onRendered(l)}
        />

        {/* ✅ every prop stable: the comparison succeeds and the child skips */}
        <Child
          label="all stable"
          style={STABLE_STYLE}
          onRendered={onRendered}
        />
      </div>

      <table style={{ marginTop: 14, fontSize: 13, borderCollapse: "collapse" }}>
        <tbody>
          {rows.map((r) => (
            <tr key={r}>
              <td style={{ paddingRight: 14 }}><code>{r}</code></td>
              <td><strong>{counts.current[r] || 0}</strong> renders</td>
            </tr>
          ))}
        </tbody>
      </table>

      <p style={{ fontSize: 13, color: "#666" }}>
        Click "re-render the parent" repeatedly. The first two counters climb
        with every click; the third stays at 1. All three are wrapped in the
        same <code>memo</code> — only the prop identities differ. The numbers
        are recorded during each commit and shown on the next one, so they
        trail the click count by one.
      </p>
    </div>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 104 of 119 decoded in the React.js track. One more won't hurt.

Back to track