DEV Community

Cover image for React Performance for Senior Developers (Practical, No Cargo Culting)
Grant Watson
Grant Watson Subscriber

Posted on

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

πŸ‘‰ Read the full article on my site

πŸ“š Browse all my articles

Author: Grant Watson

Published: 2026-02-21

Category: React / Frontend Performance


Recommended Deals

πŸ‘‰ Claim a Book-A-Million 15% coupon here


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}`);
  });
}
Enter fullscreen mode Exit fullscreen mode

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} />;
}
Enter fullscreen mode Exit fullscreen mode

Fix: stabilize identity when it matters

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

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>
  );
});
Enter fullscreen mode Exit fullscreen mode

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} />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Fix

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

  return (
    <>
      <Form form={form} setForm={setForm} />
      <Preview form={form} />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Keep frequently changing state deep.


5) Lists: virtualization beats memo spam

npm i react-window
Enter fullscreen mode Exit fullscreen mode
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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

Or use select in TanStack Query.


7) Avoid context re-render storms

Problem

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

Fix: split contexts

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

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));
  }}
/>
Enter fullscreen mode Exit fullscreen mode

Users care about input responsiveness more than filter latency.


9) Bundle performance matters

Code split heavy routes

const AdminPanel = lazy(() => import("./AdminPanel"));
Enter fullscreen mode Exit fullscreen mode

Dynamic import heavy utilities

const { jsPDF } = await import("jspdf");
Enter fullscreen mode Exit fullscreen mode

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="..."
/>
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)