Performance
Performance work should be measurement-driven: identify hotspots, fix them, then verify the win.
Most teams lose time because they optimize what is easy to change, not what is actually slow. Use measurement to keep you honest.
Where performance usually comes from
Most issues fall into one of these buckets. Once you classify the problem, you can pick the right lever.
- Rendering too much: wide rerenders from state/context changes.
- Rendering too often: high-frequency updates.
- Rendering too slowly: expensive computation or large lists.
A profiling workflow
- Reproduce a measurable scenario (scroll, typing, navigation).
- Capture a baseline: what is slow, how slow, and where time is spent.
- Fix one hotspot, then re-profile to confirm the win.
TypeScript
import { Profiler } from 'react';
export function ProfiledArea({ children }: { children: React.ReactNode }) {
return (
<Profiler
id="Main"
onRender={(
id,
phase,
actualDuration
) => {
console.log(id, phase, actualDuration);
}}
>
{children}
</Profiler>
);
}When memo helps
Memoization is useful when a component is expensive to render and receives stable props most of the time.
- Memo has a cost: comparisons, retained closures, and mental overhead. Use it where it matters.
- Stable props matter more than memo: avoid recreating objects/functions unnecessarily.
- Split state: keep high-frequency updates local so they don’t rerender large subtrees.
TypeScript
import { memo, useMemo } from 'react';
const Row = memo(function Row({ value }: { value: number }) {
return <div>{value}</div>;
});
export function List({ items }: { items: number[] }) {
const even = useMemo(() => items.filter((x) => x % 2 === 0), [items]);
return (
<div>
{even.map((x) => (
<Row key={x} value={x} />
))}
</div>
);
}Large lists
- Virtualize when the list is large and only a small window is visible.
- Prefer pagination/infinite scrolling when data is naturally chunked.
High-frequency updates and perceived performance
If typing or dragging feels laggy, the issue is often not “too much rendering” but “rendering blocks input”. Use scheduling tools to keep urgent updates responsive.
In other words: keep “urgent” updates (input echo) immediate, and let “non-urgent” updates (filtering a large list) happen later.
- `useTransition`: mark non-urgent updates so urgent input stays responsive.
- `useDeferredValue`: defer rendering derived UI from fast-changing values.
TypeScript
import { useState, useTransition } from 'react';
export function SearchBox() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const [committed, setCommitted] = useState('');
return (
<div>
<input
value={query}
onChange={(e) => {
const next = e.target.value;
setQuery(next);
startTransition(() => setCommitted(next));
}}
placeholder="Search…"
/>
<div>{isPending ? 'Loading…' : null}</div>
<Results query={committed} />
</div>
);
}
function Results({ query }: { query: string }) {
return <div>Query: {query}</div>;
}Production checklist
- Perf work is measured: you have before/after evidence.
- Hotspots are fixed at the source (state split, memo boundaries), not by blanket memo everywhere.
- Large lists are chunked/virtualized; interactions stay responsive while pending.
- Navigation has loading and error states; avoids waterfalls.
Further reading
- Guides: State Management
- Reference: useTransition / useDeferredValue
- Reference: Profiler / memo / useMemo / useCallback
- Explanation: Concurrency and Suspense