React 19 Killed Half My Performance Optimization Code, and I'm Grateful
React 19's compiler eliminated most of my useMemo and useCallback code. Table virtualization, optimistic updates, and route splitting still need manual attention.
Join the DZone community and get the full member experience.
Join For FreeI maintain a React admin dashboard codebase that had — at last count before upgrading to React 19 — 34 instances of useMemo, 28 instances of useCallback, and 19 components wrapped in memo(). I spent a nontrivial amount of time over two years adding those optimizations, debugging cases where I'd gotten the dependency arrays wrong, and explaining to junior developers why the table re-rendered on every keystroke.
React 19 with the compiler deleted most of that work. Here's what actually changed and what still matters.
The Compiler Handles What You Used to Do Manually
The React Compiler analyzes component render behavior and adds memoization where it's beneficial automatically. You don't specify it. You don't maintain dependency arrays. You don't wrap components in memo().
Before:
const Dashboard = memo(function Dashboard({ userId, filters }) {
const processedData = useMemo(
() => processData(rawData, filters),
[rawData, filters]
);
const handleFilterChange = useCallback(
(newFilter) => updateFilters(newFilter),
[updateFilters]
);
return <DataTable data={processedData} onFilter={handleFilterChange} />;
});
After React 19 with compiler:
function Dashboard({ userId, filters }) {
const processedData = processData(rawData, filters);
const handleFilterChange = (newFilter) => updateFilters(newFilter);
return <DataTable data={processedData} onFilter={handleFilterChange} />;
}
Same performance. Half the code. Zero dependency array bugs.
I removed 31 of my 34 useMemo calls after upgrading. The three I kept are genuinely complex computations where I want explicit control. Everything else the compiler handles better than I was doing manually.
The One Thing That Still Kills Dashboard Performance
The compiler doesn't solve virtualization. If your data table renders 5,000 DOM nodes because your dataset has 5,000 rows — React 19 won't fix that. The browser is still creating and painting 5,000 nodes.
Any table with more than 100 rows needs virtualization:
import { useVirtualizer } from '@tanstack/react-virtual';
export default function DataTable({ rows }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 52,
overscan: 5
});
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map(row => (
<div
key={row.index}
style={{
position: 'absolute',
top: 0,
transform: `translateY(${row.start}px)`,
height: row.size
}}
>
<TableRow data={rows[row.index]} />
</div>
))}
</div>
</div>
);
}
5,000 rows. 20 DOM nodes in the viewport at any time. Scroll is smooth. The React Compiler cannot help you here — this is a DOM problem, not a React problem.
Optimistic Updates Changed How My Dashboard Feels
The biggest perceived performance improvement in React 19 for admin dashboards isn't the compiler. It's useOptimistic.
Admin dashboards involve constant small mutations — toggling user status, updating values, changing settings. Before React 19, every mutation waited for the server response before updating the UI. Fast servers meant 200ms delays. Slow servers meant users clicking buttons twice because nothing happened visually.
'use client';
import { useOptimistic, useTransition } from 'react';
function UserStatusBadge({ user, onUpdateStatus }) {
const [optimisticStatus, setOptimisticStatus] = useOptimistic(
user.status,
(_, newStatus) => newStatus
);
const [isPending, startTransition] = useTransition();
const toggle = () => {
const newStatus = optimisticStatus === 'active' ? 'inactive' : 'active';
startTransition(async () => {
setOptimisticStatus(newStatus); // instant UI update
await onUpdateStatus(user.id, newStatus); // server in background
});
};
return (
<button
onClick={toggle}
disabled={isPending}
className={`badge ${optimisticStatus === 'active'
? 'bg-success' : 'bg-secondary'}`}
>
{optimisticStatus}
</button>
);
}
Click. Status changes instantly. The server call happens in the background. If the server fails — React reverts the optimistic update automatically. The dashboard feels instant because it is instant from the user's perspective.
I added this to every status toggle, every inline edit, every bulk action in my dashboard. The difference in perceived performance is more noticeable to users than any memoization optimization I'd done previously.
What React 19 Didn't Fix
Route-level code splitting still matters and still requires explicit configuration. If your dashboard loads all route components upfront, the initial bundle is large regardless of React version:
// Still need this in React 19
const UsersPage = lazy(() => import('./pages/Users'));
const AnalyticsPage = lazy(() => import('./pages/Analytics'));
const SettingsPage = lazy(() => import('./pages/Settings'));
Image optimization still requires next/image or equivalent. Unoptimized images are still the most common performance problem I see in dashboard codebases, and React 19 does nothing about them.
Database query performance still determines how fast your data loads. The fastest React rendering in the world doesn't compensate for a 3-second API response.
The Summary
React 19 removes the memoization overhead that made large React codebases tedious. Use the compiler, stop writing useMemo for everything, and trust that it handles the cases you were handling manually.
What still requires your attention: virtualize large tables, add optimistic updates for frequent mutations, split routes with lazy loading, and optimize your images.
The performance work that remains is more interesting than the work React 19 eliminated. That's a good trade.
Opinions expressed by DZone contributors are their own.
Comments