2/21/2026

Author: Grant Watson
Published: 2026-02-21
Category: React / Frontend Performance
Performance work fails for three reasons:
Process: measure → isolate → fix highest leverage → add guardrails
Optimize frequency first, then cost.
import { useEffect, useRef } from "react";
export function useRenderCount(name) {
const count = useRef(0);
count.current++;
useEffect(() => {
console.log(`[render] ${name}: ${count.current}`);
});
}
function Page() {
const filters = { status: "active" };
const onRowClick = (id) => setSelected(id);
return <Table filters={filters} onRowClick={onRowClick} />;
}
const filters = useMemo(() => ({ status: "active" }), []);
const onRowClick = useCallback((id) => setSelected(id), []);
Memo hooks are identity stabilizers, not magic speed boosts.
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.
function App() {
const [form, setForm] = useState({ ... });
return (
<>
<Sidebar form={form} />
<Main form={form} />
<Preview form={form} />
</>
);
}
function Main() {
const [form, setForm] = useState({ ... });
return (
<>
<Form form={form} setForm={setForm} />
<Preview form={form} />
</>
);
}
Keep frequently changing state deep.
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.
const rows = useMemo(() => {
return data.map(x => ({ ...x, computed: expensive(x) }));
}, [data]);
Or use select in TanStack Query.
<AppContext.Provider value={{ user, theme, locale, flags }}>
<BigTree />
</AppContext.Provider>
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
<LocaleContext.Provider value={locale}>
<BigTree />
</LocaleContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
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.
const AdminPanel = lazy(() => import("./AdminPanel"));
const { jsPDF } = await import("jspdf");
If it’s not needed on first paint, don’t ship it.
Best practices:
<img
src={src}
width={800}
height={450}
loading="lazy"
decoding="async"
alt="..."
/>
Performance is a process, not a one-time refactor.