← Back to Blog

React Performance for Senior Developers (Practical, No Cargo Culting)

2/21/2026

React Performance for Senior Developers (Practical, No Cargo Culting)

Author: Grant Watson
Published: 2026-02-21
Category: React / Frontend Performance


The performance mindset (the part people skip)

Performance work fails for three reasons:

  1. No baseline (you never proved it was slow)
  2. Wrong bottleneck (you optimized the wrong layer)
  3. No regression protection (you fixed it once and it came back)

Process: measure → isolate → fix highest leverage → add guardrails


1) Measure first: React Profiler + why-did-you-render

What to look for in the Profiler

  • Commit duration
  • Which components re-rendered
  • Why they re-rendered
  • Frequency vs cost

Optimize frequency first, then cost.

Render counter hook

import { useEffect, useRef } from "react";

export function useRenderCount(name) {
  const count = useRef(0);
  count.current++;

  useEffect(() => {
    console.log(`[render] ${name}: ${count.current}`);
  });
}

2) The #1 cause of slow apps: unnecessary re-renders

Problem: unstable props

function Page() {
  const filters = { status: "active" };
  const onRowClick = (id) => setSelected(id);

  return <Table filters={filters} onRowClick={onRowClick} />;
}

Fix: stabilize identity when it matters

const filters = useMemo(() => ({ status: "active" }), []);
const onRowClick = useCallback((id) => setSelected(id), []);

Memo hooks are identity stabilizers, not magic speed boosts.


3) React.memo: use it on expensive components

const Row = React.memo(function Row({ item, onClick }) {
  return (
    <div onClick={() => onClick(item.id)}>
      {item.name} — {item.status}
    </div>
  );
});

Memo only works if the props are stable.


4) State architecture: colocate hot state

Problem

function App() {
  const [form, setForm] = useState({ ... });

  return (
    <>
      <Sidebar form={form} />
      <Main form={form} />
      <Preview form={form} />
    </>
  );
}

Fix

function Main() {
  const [form, setForm] = useState({ ... });

  return (
    <>
      <Form form={form} setForm={setForm} />
      <Preview form={form} />
    </>
  );
}

Keep frequently changing state deep.


5) Lists: virtualization beats memo spam

npm i react-window
import { FixedSizeList as List } from "react-window";

function VirtualizedUsers({ users }) {
  return (
    <List
      height={600}
      itemCount={users.length}
      itemSize={44}
      width="100%"
      itemData={users}
    >
      {Row}
    </List>
  );
}

High ROI change: fewer DOM nodes, less layout, less memory.


6) Memoize derived data

const rows = useMemo(() => {
  return data.map(x => ({ ...x, computed: expensive(x) }));
}, [data]);

Or use select in TanStack Query.


7) Avoid context re-render storms

Problem

<AppContext.Provider value={{ user, theme, locale, flags }}>
  <BigTree />
</AppContext.Provider>

Fix: split contexts

<UserContext.Provider value={user}>
  <ThemeContext.Provider value={theme}>
    <LocaleContext.Provider value={locale}>
      <BigTree />
    </LocaleContext.Provider>
  </ThemeContext.Provider>
</UserContext.Provider>

8) Concurrent React: keep typing responsive

const [query, setQuery] = useState("");
const [filter, setFilter] = useState("");
const [isPending, startTransition] = useTransition();

<input
  value={query}
  onChange={(e) => {
    const next = e.target.value;
    setQuery(next);
    startTransition(() => setFilter(next));
  }}
/>

Users care about input responsiveness more than filter latency.


9) Bundle performance matters

Code split heavy routes

const AdminPanel = lazy(() => import("./AdminPanel"));

Dynamic import heavy utilities

const { jsPDF } = await import("jspdf");

If it’s not needed on first paint, don’t ship it.


10) Images: silent performance killers

Best practices:

  • Responsive sizes
  • AVIF/WebP
  • Lazy load below the fold
  • Set width/height to prevent CLS
<img
  src={src}
  width={800}
  height={450}
  loading="lazy"
  decoding="async"
  alt="..."
/>

11) Guardrails against regressions

  • Track bundle size in CI
  • Add render sanity checks for critical screens
  • Don’t ship multi‑MB JS for simple views

The practical checklist

  1. Profile first
  2. Fix frequency re-renders
  3. Virtualize large lists
  4. Split contexts and colocate hot state
  5. Code split heavy bundles
  6. Add guardrails

Performance is a process, not a one-time refactor.


Land Your Next $100k Job with Ladders