Skip to the solution
HardFrontend7 min read

How do you optimize React component rendering?

929 views

The problem

Question presented to candidate: "Forget how often a component renders. When one single render is slow, what do you do?"

What a strong answer should cover:

  • Distinguish the two axes immediately: how often a component renders and how much one render costs. They have different fixes.
  • Measure the second with the Profiler's actualDuration, or the Profiler component's onRender callback.
  • What makes a single render expensive: heavy computation in the render body, creating very large element trees, deep prop drilling causing wide subtree renders, and expensive derived data.
  • Move computation out of render: useMemo for derived values, or compute it once outside the component if it does not depend on props.
  • Reduce the tree: virtualise long lists — the largest single win available, since it changes the element count by orders of magnitude.
  • Split the component so the expensive part is isolated and can be skipped independently.
  • Defer rather than shrink: useTransition and useDeferredValue keep input responsive while an expensive render happens at lower priority.
  • Know the difference between actualDuration and baseDuration — the latter is what it would cost with no memoisation.
  • Development builds are substantially slower; profile a production-profiling build.

Clarifying questions expected:

  • "Is one render slow, or are there too many?" — that is the whole fork in this question.
  • "How large is the tree we are rendering?"

Code / implementation expected: Yes — a Profiler measuring an expensive render, and the fixes applied.

  • performance
  • optimization
  • memoization
  • react.memo

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

7 min read · 10 sections

Try it in the playground

Open any example in the playground to run and change it.

ReactMeasuring render cost with Profiler, then cutting it three ways
import { useState, useRef, useMemo, useDeferredValue, Profiler } from "react";

// Module scope: computed ONCE when the module loads, not per render.
const ROWS = Array.from({ length: 3000 }, (_, i) => ({
  id: i,
  name: "Item " + i,
  score: (i * 7919) % 1000,
}));

function expensiveScore(row) {
  let x = 0;
  for (let i = 0; i < 400; i++) x += (row.score * i) % 13;
  return x;
}

// ❌ Everything computed inline, every row rendered.
function Naive({ query }) {
  const rows = ROWS
    .filter((r) => r.name.includes(query))
    .map((r) => ({ ...r, computed: expensiveScore(r) }))
    .sort((a, b) => a.computed - b.computed);
  return <Rows rows={rows} limit={rows.length} />;
}

// ✅ Memoised derivation + a windowed slice: far less work AND far fewer
//    elements. The element-count reduction is the bigger win of the two.
function Optimised({ query }) {
  const rows = useMemo(
    () =>
      ROWS.filter((r) => r.name.includes(query))
        .map((r) => ({ ...r, computed: expensiveScore(r) }))
        .sort((a, b) => a.computed - b.computed),
    [query],
  );
  return <Rows rows={rows} limit={30} />;
}

function Rows({ rows, limit }) {
  return (
    <div style={{ height: 120, overflow: "auto", border: "1px solid #ddd", borderRadius: 6, fontSize: 12 }}>
      {rows.slice(0, limit).map((r) => (
        <div key={r.id} style={{ padding: "1px 6px" }}>{r.name} — {r.computed}</div>
      ))}
      <div style={{ padding: "2px 6px", color: "#666" }}>
        rendering {Math.min(limit, rows.length)} of {rows.length}
      </div>
    </div>
  );
}

export default function App() {
  const [query, setQuery] = useState("");
  const [timings, setTimings] = useState({});
  // Same work, lower priority: typing stays responsive.
  const deferred = useDeferredValue(query);

  // Recording a timing into state causes another render, which fires onRender
  // again — an infinite loop unless you stop it. Record once per query value.
  const recorded = useRef({});
  const record = (id, phase, actualDuration) => {
    const key = id + ":" + deferred;
    if (recorded.current[key]) return;
    recorded.current[key] = true;
    setTimings((t) => ({ ...t, [id]: Math.round(actualDuration * 100) / 100 }));
  };

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 540 }}>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="filter — try typing 1, 12, 123"
        style={{ width: "100%", marginBottom: 10 }}
      />

      <h4 style={{ margin: "0 0 4px" }}>
        ❌ naive — <span style={{ color: "#a33" }}>{timings.naive ?? "…"}ms</span> per render
      </h4>
      <Profiler id="naive" onRender={record}>
        <Naive query={deferred} />
      </Profiler>

      <h4 style={{ margin: "12px 0 4px" }}>
        ✅ memoised + windowed — <span style={{ color: "#161" }}>{timings.optimised ?? "…"}ms</span> per render
      </h4>
      <Profiler id="optimised" onRender={record}>
        <Optimised query={deferred} />
      </Profiler>

      <p style={{ color: "#666", fontSize: 13 }}>
        Both render exactly once per keystroke — the difference is entirely
        cost per render. Most of the saving comes from rendering 30 elements
        instead of 3000, not from the memoisation.
      </p>
    </div>
  );
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 78 of 119 in React.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All React.js questions
Up nextHard

How do you avoid re-renders with composition instead of memoization?

Continue