π Read the full article on my site
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:
- No baseline (you never proved it was slow)
- Wrong bottleneck (you optimized the wrong layer)
- 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
- Profile first
- Fix frequency re-renders
- Virtualize large lists
- Split contexts and colocate hot state
- Code split heavy bundles
- Add guardrails
Performance is a process, not a one-time refactor.
Top comments (0)