React performance optimization changed significantly in 2026. The React Compiler — stable since React 19 — automatically handles the memoization that developers used to write by hand. This means the checklist is shorter than it was in 2023, but the remaining items matter more.
What changed in 2026
- React Compiler is stable and widely adopted. It inserts
useMemo and useCallback at the IR level — components compiled with it often need no manual memoization at all.
- React 19 Server Components are the default in Next.js 15+. Rendering on the server eliminates entire categories of client-side performance problems by never sending the component to the browser.
- Concurrent features are the default.
useTransition, useDeferredValue, and automatic batching are enabled everywhere, not just opt-in.
- TanStack Virtual replaced react-window as the community standard for list virtualization.
Step 1 — profile first
Open React DevTools, go to the Profiler tab, click Record, interact with the slow part of your UI, then stop. You will see a flame chart of every render. Look for:
- Gray bars — components that rendered but did not change (unnecessary re-renders).
- Tall stacks — deep component trees rendering on every keystroke.
- Commit times over 16 ms — anything above this drops below 60 fps.
Do not optimize before profiling. "It feels slow" is not a measurement.
Step 2 — check if React Compiler is active
# If you are on Next.js 15+
# next.config.ts
export default {
experimental: { reactCompiler: true },
};
With the compiler enabled, unnecessary useMemo/useCallback calls should be removed from your codebase — they add noise and the compiler outperforms manual annotations.
Verify with the React DevTools "Compiler" badge — components optimized by the compiler show a small "✓" in the Profiler.
Step 3 — code-split with lazy and Suspense
import { lazy, Suspense } from "react";
const HeavyChart = lazy(() => import("./HeavyChart"));
export function Dashboard() {
return (
<Suspense fallback={<p>Loading chart…</p>}>
<HeavyChart />
</Suspense>
);
}
This keeps HeavyChart out of the initial bundle. Combined with route-level splitting (automatic in Next.js), most apps shed 30–50% of initial JavaScript.
Step 4 — virtualize long lists
import { useVirtualizer } from "@tanstack/react-virtual";
import { useRef } from "react";
export function BigList({ items }: { items: string[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 40,
});
return (
<div ref={parentRef} style={{ height: 400, overflow: "auto" }}>
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map((item) => (
<div
key={item.index}
style={{ position: "absolute", top: item.start, height: 40 }}
>
{items[item.index]}
</div>
))}
</div>
</div>
);
}
Renders ~15 DOM nodes regardless of list length. Essential for lists over ~200 items.
When manual memoization still helps
Even with the compiler, some cases need explicit intervention:
| Scenario |
Fix |
| Expensive pure computation in render |
useMemo(() => heavyCalc(data), [data]) |
| Callback passed to non-compiled third-party lib |
useCallback |
| Context value object recreated every render |
Memoize the value or split context |
| Non-React canvas/WebGL update loop |
useRef + useEffect, not state |
Step 5 — minimize context re-renders
// Bad: every consumer re-renders when ANY part of this object changes
const ctx = { user, theme, cart };
// Good: split into separate contexts
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
<CartContext.Provider value={cart}>
{children}
</CartContext.Provider>
</ThemeContext.Provider>
</UserContext.Provider>
Or use a state management library (Zustand, Jotai) where components subscribe only to the slices they read.
Render performance quick reference
| Pattern |
Impact |
Effort |
| Code splitting with lazy() |
High |
Low |
| List virtualization |
High (for long lists) |
Low |
| Enable React Compiler |
High |
Low |
| Split context by concern |
Medium |
Low |
| useDeferredValue for input-driven renders |
Medium |
Low |
| Manual useMemo on heavy calculations |
Low–Medium |
Medium |
| Move state down (closer to consumer) |
Medium |
Medium |
How to pick your first optimization
- Profiler shows slow initial load? → Add code splitting.
- Profiler shows a list renders slowly? → Add virtualization.
- Lots of gray "unnecessary" re-renders? → Check if the compiler is on; then look for context issues.
- Specific computation is slow? →
useMemo on that computation.
- Input typing feels laggy? →
useDeferredValue to defer the expensive downstream render.
Common mistakes
Premature React.memo on everything. It adds a shallow-comparison cost on every render. With the compiler it is outright counterproductive.
Mutating state directly. items.push(x); setState(items) — the reference does not change, React bails out, the UI does not update. Always return a new reference.
Large context values that change frequently. Putting { user, settings, cart } in one context means every consumer re-renders on every cart update. Split them.
Ignoring bundle size. A 1 MB JS bundle with no code splitting will always feel slow on mobile regardless of component optimization.
What to skip
- Manual memoization everywhere — let the compiler do it; audit with the Profiler if you have a real problem.
- Rewriting to class components for performance — they offer no advantage in 2026.
- Micro-benchmarking synthetic counters — measure your actual app; synthetic benchmarks do not predict real UX.
FAQ
Is React Compiler a drop-in for all projects?
Mostly yes for React 19+ projects, but some patterns using mutable refs or direct DOM manipulation need adjustment. Run the compiler in "annotation-only" mode first to see what it flags.
Should I still use React.memo?
Only for components not covered by the compiler (e.g. third-party components, or if your project does not use the compiler yet). Profile first.
How does useTransition help performance?
It marks a state update as non-urgent, so React can interrupt it to handle more pressing updates like keystrokes. Useful for expensive filter/search renders.
What is the biggest bang-for-buck optimization?
For most apps: enable the React Compiler and add route-level code splitting. Those two changes alone often halve TTI without touching individual components.
Where to go next