React 19 changed everything about performance optimization. The new React Compiler handles most memoization automatically, which means the optimization patterns you learned in React 18 are now mostly unnecessary.
But "mostly" isn't "always." This guide explains what's changed, what still matters, and practical patterns for building fast React apps that score well on Core Web Vitals.
What changed in React 19
The React Compiler
React 19's biggest performance feature isn't a hook - it's the compiler. During build time, it analyzes your components and automatically:
- Skips unnecessary re-renders
- Memoizes expensive calculations
- Stabilizes function references
- Optimizes prop comparisons
This happens without you writing any optimization code. The compiler figures out what needs memoization and adds it for you.
// React 18: You wrote this
const ExpensiveComponent = memo(({ data }) => {
const processed = useMemo(() => heavyCalculation(data), [data]);
const handleClick = useCallback(() => doSomething(data), [data]);
return <div onClick={handleClick}>{processed}</div>;
});
// React 19: Just write this
function ExpensiveComponent({ data }) {
const processed = heavyCalculation(data);
const handleClick = () => doSomething(data);
return <div onClick={handleClick}>{processed}</div>;
}
// The compiler adds memoization where needed
What this means for you
Write simpler code. Don't add useMemo, useCallback, or memo by default. Let the compiler optimize.
Profile before optimizing. If you see a performance issue, then investigate. Don't preemptively add hooks "just in case."
The hooks still exist. They're not deprecated. There are cases where manual control is needed.
When you still need manual optimization
The compiler is smart, but it's not magic. Here's when manual optimization still matters:
1. Third-party library constraints
Some libraries expect stable references. If a library re-initializes when a callback changes, you need useCallback:
function MapComponent({ markers }) {
// MapLibrary re-initializes if onMarkerClick changes
// Compiler might not catch this library-specific behavior
const onMarkerClick = useCallback((marker) => {
console.log('Clicked:', marker.id);
}, []);
return <MapLibrary markers={markers} onMarkerClick={onMarkerClick} />;
}
2. Effects with function dependencies
When a function is in an effect's dependency array, you might need useCallback to prevent infinite loops:
function DataFetcher({ userId }) {
const [data, setData] = useState(null);
// Without useCallback, this creates a new function every render
// causing the effect to run infinitely
const fetchData = useCallback(async () => {
const result = await api.getUser(userId);
setData(result);
}, [userId]);
useEffect(() => {
fetchData();
}, [fetchData]);
return <div>{data?.name}</div>;
}
Better pattern: Move the function inside the effect when possible:
function DataFetcher({ userId }) {
const [data, setData] = useState(null);
useEffect(() => {
async function fetchData() {
const result = await api.getUser(userId);
setData(result);
}
fetchData();
}, [userId]);
return <div>{data?.name}</div>;
}
3. Expensive calculations with external data
If a calculation depends on external data that changes frequently, you might want explicit memoization:
function SearchResults({ query, allItems }) {
// allItems has 10,000 entries and changes rarely
// query changes on every keystroke
// Compiler might not optimize this perfectly
const filteredItems = useMemo(() => {
return allItems.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
);
}, [query, allItems]);
return <List items={filteredItems} />;
}
4. Context value stability
Context values that are objects need memoization to prevent all consumers re-rendering:
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
// Without useMemo, this object is recreated every render
// causing all useContext(ThemeContext) consumers to re-render
const value = useMemo(() => ({
theme,
setTheme,
isDark: theme === 'dark',
}), [theme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
Patterns that actually improve performance
Lazy loading components
Don't load everything upfront. Split your bundle and load components when needed:
import { lazy, Suspense } from 'react';
// Loaded only when rendered
const HeavyChart = lazy(() => import('./HeavyChart'));
const AdminPanel = lazy(() => import('./AdminPanel'));
function Dashboard({ user }) {
return (
<div>
<Header user={user} />
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart data={user.analytics} />
</Suspense>
{user.isAdmin && (
<Suspense fallback={<Loading />}>
<AdminPanel />
</Suspense>
)}
</div>
);
}
This directly improves LCP by reducing initial JavaScript. If you're using Material UI, lazy loading heavy components like DataGrid and DatePicker is essential - see our MUI performance guide for specifics.
Virtualize long lists
Rendering thousands of items kills performance. Use virtualization:
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
});
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
transform: `translateY(${virtualItem.start}px)`,
height: `${virtualItem.size}px`,
}}
>
{items[virtualItem.index].name}
</div>
))}
</div>
</div>
);
}
Optimize images properly
Image optimization has a bigger impact than most React patterns. Use proper loading strategies:
function ProductImage({ src, alt, isLCP }) {
return (
<img
src={src}
alt={alt}
// LCP images should load eagerly
loading={isLCP ? 'eager' : 'lazy'}
// Prevent CLS
width={400}
height={300}
// Hint browser about importance
fetchPriority={isLCP ? 'high' : 'auto'}
/>
);
}
For Next.js users, use next/image which handles this automatically:
import Image from 'next/image';
function HeroImage({ src }) {
return (
<Image
src={src}
alt="Hero"
width={1200}
height={600}
priority // Marks as LCP image
/>
);
}
Debounce expensive operations
For search inputs or other frequent updates, debounce to reduce work:
import { useDeferredValue } from 'react';
function SearchableList({ items }) {
const [query, setQuery] = useState('');
// React 19: useDeferredValue handles this elegantly
const deferredQuery = useDeferredValue(query);
// This expensive filter uses the deferred value
// UI stays responsive while filtering happens in background
const filteredItems = items.filter(item =>
item.name.toLowerCase().includes(deferredQuery.toLowerCase())
);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
{/* Show stale indicator while updating */}
<div style={{ opacity: query !== deferredQuery ? 0.7 : 1 }}>
<List items={filteredItems} />
</div>
</div>
);
}
Use transitions for non-urgent updates
Mark state updates as non-urgent so they don't block user input:
import { useTransition } from 'react';
function TabContainer() {
const [tab, setTab] = useState('home');
const [isPending, startTransition] = useTransition();
function handleTabChange(newTab) {
startTransition(() => {
setTab(newTab); // This update won't block input
});
}
return (
<div>
<TabButtons
onSelect={handleTabChange}
disabled={isPending}
/>
<div style={{ opacity: isPending ? 0.7 : 1 }}>
<TabContent tab={tab} />
</div>
</div>
);
}
This improves INP by keeping interactions responsive.
Performance anti-patterns to avoid
Don't: Premature optimization
// Bad: Adding hooks "just in case"
function SimpleButton({ onClick, children }) {
const memoizedOnClick = useCallback(onClick, [onClick]);
return <button onClick={memoizedOnClick}>{children}</button>;
}
// Good: Keep it simple
function SimpleButton({ onClick, children }) {
return <button onClick={onClick}>{children}</button>;
}
The compiler handles this. Don't add complexity for no measured benefit.
Don't: State in wrong location
// Bad: Parent re-renders everything on input change
function Parent() {
const [search, setSearch] = useState('');
return (
<div>
<SearchInput value={search} onChange={setSearch} />
<ExpensiveList /> {/* Re-renders on every keystroke */}
<AnotherComponent />
</div>
);
}
// Good: Co-locate state with component that uses it
function Parent() {
return (
<div>
<SearchWithResults /> {/* State lives here */}
<ExpensiveList />
<AnotherComponent />
</div>
);
}
function SearchWithResults() {
const [search, setSearch] = useState('');
// Only this component re-renders on input
return (
<>
<SearchInput value={search} onChange={setSearch} />
<Results query={search} />
</>
);
}
Don't: Inline object/array creation in JSX
// Bad: New object every render
<Component style={{ marginTop: 20 }} />
<List items={[1, 2, 3]} />
// Good: Define outside component or use useMemo if dynamic
const style = { marginTop: 20 };
const items = [1, 2, 3];
<Component style={style} />
<List items={items} />
This matters when passing to memoized children. It's especially relevant for MUI's sx prop - using sx in loops can add significant overhead. See MUI performance optimization for when to use styled() instead.
Measuring React performance
Don't optimize blindly. Use these tools:
React DevTools Profiler
- Install React DevTools browser extension
- Open DevTools → Profiler tab
- Click record, interact with your app, stop recording
- Look for components that render often or take long
Chrome DevTools Performance
For Core Web Vitals specifically:
- Open DevTools → Performance tab
- Record a page load
- Look at the "Timings" section for LCP, CLS markers
- Check "Main" thread for long tasks affecting INP
Web Vitals library
Add real user monitoring:
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(console.log);
onINP(console.log);
onCLS(console.log);
Checklist: React + Core Web Vitals
For LCP
- Lazy load below-fold components with
React.lazy - Use
priorityon Next.js Image for hero images - Minimize JavaScript bundle size
- Server-side render critical content
For CLS
- Set explicit dimensions on images
- Reserve space for dynamic content
- Avoid inserting content above existing content
- Use CSS
transformfor animations, not layout properties
For INP
- Use
useTransitionfor non-urgent updates - Use
useDeferredValuefor expensive computations - Virtualize long lists
- Keep event handlers fast, move heavy work to effects
Frequently Asked Questions
Is useMemo obsolete in React 19?
Not obsolete, but rarely needed. The React Compiler handles most memoization automatically. Only add useMemo manually when you've measured a specific performance issue that the compiler doesn't catch.
Should I remove all my existing useMemo and useCallback hooks?
Not immediately. They still work and won't hurt performance. But for new code, start without them and only add if needed. Gradually simplify existing code when you're confident the compiler handles it.
How do I know if the React Compiler is working?
Check your build output for the compiler's transformations, or use React DevTools which shows when components skip re-renders. If you're on React 19 with a modern build setup, it's likely active.
What's the biggest performance win in React?
Usually it's not React-specific: optimize images, reduce JavaScript bundle size, and server-render critical content. These affect Core Web Vitals more than any hook optimization.
Does React 19 help with INP?
Yes. The automatic memoization reduces unnecessary re-renders, and features like useTransition and useDeferredValue help keep interactions responsive. But you still need to use them correctly.
What's next
React performance starts with measurement. Before optimizing anything, run your site through PageSpeedFix to identify what's actually hurting your Core Web Vitals.
Often the fix isn't a React pattern at all - it's an unoptimized image, render-blocking script, or missing server-side rendering. Fix the big issues first, then fine-tune React-specific patterns if needed.
Framework and library guides:
- Material UI Performance - Fix slow renders and large bundles in MUI apps
- Vite + React Performance - Fix slow dev server and optimize production bundles
- Next.js Core Web Vitals - Optimize your Next.js app for performance