Skip to solution
mediumFrontend

How do you optimize performance in a React application?

228 views
01

Understand the problem

Question presented to candidate: "An app feels slow. How do you approach making it faster?"

What a strong answer should cover:

  • Measure before changing anything, and measure the right thing: Lighthouse and Core Web Vitals for load, the DevTools Profiler for interaction, the Network panel for data.
  • The categories are distinct and easily confused: load performance, network, render performance, and memory.
  • Load: code splitting with React.lazy at route boundaries, tree shaking, checking the bundle for accidentally-included heavy dependencies, and server rendering for first paint.
  • Network: eliminating waterfalls with Promise.all and loaders, caching with a query library, and preloading.
  • Render: structural fixes first (move state down, lift content up), then memoisation, then context splitting.
  • Lists: virtualisation, which is a different order of magnitude from memoisation.
  • Concurrent features: useTransition and useDeferredValue to keep input responsive during expensive updates — perceived performance rather than less work.
  • Images and fonts are frequently the real problem and have nothing to do with React.
  • Know which metric you are moving: LCP, INP, and CLS measure different things.

Clarifying questions expected:

  • "Slow to load, or slow to interact? Those are completely different problems."
  • "Do we have real user metrics, or is this anecdotal?"
  • "Which device and network are we targeting?"

Code / implementation expected: Optional — route-level code splitting and a transition are the two most demonstrable.

performanceoptimizationmemoizationrendering
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 React interviews — assumes broad React familiarity. Difficulty: Medium

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. This is a breadth question, so it leans on measurements taken elsewhere i

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

Code splitting, virtualisation, and a transition keeping input responsive
Run Playground
import { useState, useMemo, useDeferredValue, useTransition, lazy, Suspense } from "react";

// ── LOAD: a route-level split point. The bundler emits a separate chunk that
//    is only fetched when this actually renders. ─────────────────────────────
const HeavyPanel = lazy(
  () => new Promise((resolve) =>
    setTimeout(() => resolve({
      default: () => (
        <div style={{ background: "#eef", padding: 10, borderRadius: 6 }}>
          Heavy panel — arrived as its own chunk, not in the initial bundle.
        </div>
      ),
    }), 700)),
);

const ROWS = Array.from({ length: 5000 }, (_, i) => "Row " + i + " — item name here");

// ── RENDER: a hand-rolled window. Only the visible slice is rendered, which is
//    a different order of magnitude from memoising 5000 rows. ───────────────
const ROW_H = 24;
const WINDOW = 300;

function VirtualList({ rows }) {
  const [scrollTop, setScrollTop] = useState(0);
  const start = Math.max(0, Math.floor(scrollTop / ROW_H) - 3);
  const visible = rows.slice(start, start + Math.ceil(WINDOW / ROW_H) + 6);

  return (
    <div
      onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}
      style={{ height: WINDOW, overflow: "auto", border: "1px solid #ddd", borderRadius: 6 }}
    >
      {/* A spacer gives the scrollbar the full height */}
      <div style={{ height: rows.length * ROW_H, position: "relative" }}>
        {visible.map((r, i) => (
          <div key={start + i} style={{
            position: "absolute", top: (start + i) * ROW_H, height: ROW_H,
            fontSize: 13, lineHeight: ROW_H + "px", paddingLeft: 8,
          }}>
            {r}
          </div>
        ))}
      </div>
      <p style={{ position: "sticky", bottom: 0, background: "#fff", margin: 0, fontSize: 12, padding: 4 }}>
        rendering <strong>{visible.length}</strong> of {rows.length} rows
      </p>
    </div>
  );
}

export default function App() {
  const [showHeavy, setShowHeavy] = useState(false);
  const [query, setQuery] = useState("");
  const [isPending, startTransition] = useTransition();

  // PERCEIVED: the deferred value lags behind, so typing never waits for the
  // expensive filter. The same work happens — just at a lower priority.
  const deferred = useDeferredValue(query);
  const filtered = useMemo(
    () => ROWS.filter((r) => r.toLowerCase().includes(deferred.toLowerCase())),
    [deferred],
  );

  return (
    <div style={{ padding: 24, fontFamily: "system-ui", lineHeight: 1.6, maxWidth: 540 }}>
      <h4 style={{ marginTop: 0 }}>Code splitting</h4>
      <button onClick={() => setShowHeavy((s) => !s)}>
        {showHeavy ? "hide" : "load"} the heavy panel
      </button>
      {showHeavy && (
        <Suspense fallback={<p style={{ color: "#888" }}>fetching chunk…</p>}>
          <HeavyPanel />
        </Suspense>
      )}

      <h4>Virtualised list of {ROWS.length} rows</h4>
      <input
        value={query}
        onChange={(e) => startTransition(() => setQuery(e.target.value))}
        placeholder="filter — typing stays responsive"
        style={{ width: "100%", marginBottom: 6 }}
      />
      <div style={{ opacity: isPending || query !== deferred ? 0.6 : 1, transition: "opacity .15s" }}>
        <VirtualList rows={filtered} />
      </div>

      <p style={{ color: "#666", fontSize: 13 }}>
        Scroll the list: only about a dozen rows exist in the DOM at any moment.
        Type quickly in the filter: the input never stutters, because the
        expensive filter runs at a lower priority and the list dims while it
        catches up.
      </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 65 of 119 decoded in the React.js track. One more won't hurt.

Back to track