React 19+ rendering performance: React Compiler diagnostics, profiler-driven optimization, useTransition for non-blocking updates, the new Activity and ViewTransition components, resource preloading APIs, and when to actually reach for useMemo/useCallback vs letting the Compiler handle it. Use when debugging slow renders, optimizing user-perceived performance, or implementing smooth navigation transitions in React 19+ applications.
Scanned 9/3/2026
Install to Claude Code
npx -y skills add opensite-ai/opensite-skills --skill react-rendering-performance --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of React Rendering Performance?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/opensite-ai-react-rendering-performance)More formats (shields.io, HTML) on the badges page.
---
name: react-rendering-performance
description: >
React 19+ rendering performance: React Compiler diagnostics, profiler-driven
optimization, useTransition for non-blocking updates, the new Activity and
ViewTransition components, resource preloading APIs, and when to actually
reach for useMemo/useCallback vs letting the Compiler handle it. Use when
debugging slow renders, optimizing user-perceived performance, or implementing
smooth navigation transitions in React 19+ applications.
compatibility: >
Requires React 19+ code and, ideally, React DevTools Profiler or runtime
traces for measurement.
metadata:
opensite-category: frontend
opensite-scope: shared
opensite-visibility: public
---
# React Rendering Performance
## Skill Resources
- Activation and cross-agent notes: [references/activation.md](references/activation.md)
- Use `ultrathink` or the deepest available reasoning mode before changing architecture, security, migration, or performance-critical paths.
- Template: [templates/profiler-session.md](templates/profiler-session.md)
Performance work without measurement is guessing. The correct sequence: measure with the Profiler, identify the bottleneck type, then apply the right tool. Memoizing everything is not a strategy.
---
## React 19 Compiler: What Changed
React Compiler (stable in React 19) automatically memoizes components, values, and callbacks. It analyzes the component tree statically and inserts `useMemo`, `useCallback`, and `React.memo` where they would help — without you writing them.
### What the Compiler Handles Automatically
```jsx
// ❌ Old React 18: Manual memoization required
const expensiveResult = useMemo(() => compute(data), [data]);
const stableCallback = useCallback(() => onSelect(id), [id, onSelect]);
const MemoizedChild = React.memo(({ value }) => <div>{value}</div>);
// ✅ React 19 + Compiler: Write normal code, Compiler memoizes when beneficial
const expensiveResult = compute(data); // Compiler inserts memo if compute is expensive
const handleSelect = () => onSelect(id); // Compiler stabilizes if beneficial
function Child({ value }) { return <div>{value}</div>; } // Compiler adds memo if needed
```
### When You Still Need Manual Memoization
The Compiler cannot memoize:
- Third-party library calls with opaque costs
- References that must be stable for external reasons (WebSocket listeners, event emitters)
- Computations the Compiler cannot prove are pure
```jsx
// Use manual memo for external listener stability
const stableHandler = useCallback((event) => {
externalEventEmitter.emit('change', event);
}, []); // Compiler may not stabilize this without the hint
useEffect(() => {
externalEventEmitter.on('input', stableHandler);
return () => externalEventEmitter.off('input', stableHandler);
}, [stableHandler]);
```
### Diagnosing Compiler Output
```bash
# Install the Compiler (Next.js 15+ enables it automatically)
npm install --save-dev babel-plugin-react-compiler
# Check which components the Compiler skipped and why
# In browser console after enabling compiler logging:
# REACT_COMPILER_DEVTOOLS=true npm run dev
```
The Compiler skips components that:
- Mutate props or state directly
- Use `arguments` object
- Call hooks conditionally
- Have unrecognized patterns
Fix the anti-pattern to let the Compiler optimize it.
---
## Chrome DevTools: React Performance Tracks (React 19.2)
React 19.2 adds React-specific lanes to Chrome's Performance panel, showing scheduler priorities, render timing, and effect execution directly alongside browser-level data.
```
Performance Panel → Record → Interact → Stop
↓
Look for the "React" track group (new in 19.2):
- "Render" lanes: component tree render time per update
- "Commit" lanes: DOM mutation and effect duration
- "Scheduler" lanes: task priority and yielding behavior
```
Correlate React render timing with "Long Tasks" in the browser track to find renders that block the main thread.
---
## React Profiler: Finding Real Bottlenecks
Use the React DevTools Profiler to find which component is slow before writing any optimization code.
```jsx
import { Profiler } from 'react';
function onRenderCallback(
id, // Component name
phase, // "mount" | "update" | "nested-update"
actualDuration, // Time for this render in ms
baseDuration, // Estimated time without memoization
startTime,
commitTime,
) {
if (actualDuration > 16) { // > 16ms = missed a frame
console.warn(`[Profiler] Slow render in ${id}: ${actualDuration.toFixed(1)}ms (phase: ${phase})`);
}
}
// Wrap the component tree you're investigating
<Profiler id="OrderList" onRender={onRenderCallback}>
<OrderList orders={orders} />
</Profiler>
```
### Reading Profiler Output
- **`actualDuration` >> `baseDuration`**: Memoization is working but a dependency changed unnecessarily — track down the unstable reference
- **`actualDuration` ≈ `baseDuration`**: Component renders fully every time — check if it should be memoized or if its parent re-renders too often
- **High `baseDuration`**: The component's own render logic is expensive — profile the inner computation
---
## `useTransition` for Non-Blocking Updates
Marking a state update as a Transition tells React it's non-urgent — the browser can interrupt it to handle user input (like typing) without waiting for the transition to finish.
```jsx
import { useTransition, useState } from 'react';
function SearchResults() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
function handleSearch(input) {
// Typing the query is urgent — update immediately
setQuery(input);
// Rendering 10,000 search results is non-urgent — can be interrupted
startTransition(() => {
setResults(filterResults(input, allData));
});
}
return (
<>
<input value={query} onChange={e => handleSearch(e.target.value)} />
{isPending ? (
<div aria-label="Loading results...">Searching...</div>
) : (
<ResultList results={results} />
)}
</>
);
}
```
### React 19: `useTransition` Now Accepts Async Functions
```jsx
const [isPending, startTransition] = useTransition();
async function handleSubmit(formData) {
startTransition(async () => {
// Async actions in transitions: React keeps isPending=true until resolved
const result = await submitToServer(formData);
setServerResult(result);
});
}
```
---
## `useActionState` for Async Form Performance
Replaces the `isLoading` / `isError` / `result` state pattern with a single hook. Eliminates redundant state updates that trigger unnecessary re-renders.
```jsx
import { useActionState } from 'react';
// ❌ Old pattern: 3 state variables, each triggers a render
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [result, setResult] = useState(null);
// ✅ React 19: single hook, single render per state change
const [state, submitAction, isPending] = useActionState(
async (prevState, formData) => {
const name = formData.get('name');
const result = await createRestaurant({ name });
return { success: true, restaurant: result };
},
null, // initial state
);
```
---
## `useOptimistic`: Instant UI Updates
For mutations where you're confident the server will succeed, apply the update immediately and roll back only on failure.
```jsx
import { useOptimistic } from 'react';
function ReviewList({ reviews, onAddReview }) {
const [optimisticReviews, addOptimisticReview] = useOptimistic(
reviews,
(currentReviews, newReview) => [...currentReviews, newReview],
);
async function handleSubmit(formData) {
const optimisticEntry = {
id: crypto.randomUUID(),
text: formData.get('text'),
rating: Number(formData.get('rating')),
pending: true,
};
addOptimisticReview(optimisticEntry); // Instant UI update
try {
await onAddReview(formData); // If this fails, React reverts automatically
} catch {
// React automatically rolls back to the original reviews array
}
}
return (
<ul>
{optimisticReviews.map(r => (
<li key={r.id} style={{ opacity: r.pending ? 0.6 : 1 }}>
{r.text}
</li>
))}
</ul>
);
}
```
---
## `useEffectEvent`: Stop Unnecessary Effect Re-runs
Effects that close over props/state re-run when those values change, even when the closure behavior doesn't need to change. `useEffectEvent` separates "event logic" from "effect dependencies" (React 19.2).
```jsx
import { useEffect, useEffectEvent } from 'react';
function Analytics({ userId, currentTheme }) {
// ❌ Without useEffectEvent: theme changes cause reconnect
useEffect(() => {
const socket = connect(userId);
socket.on('event', (data) => {
// currentTheme is captured here, causing effect to re-run when theme changes
logEvent({ data, theme: currentTheme });
});
return () => socket.disconnect();
}, [userId, currentTheme]); // theme in deps = reconnect on theme change
// ✅ useEffectEvent: theme reads are "event" logic, not effect deps
const onEvent = useEffectEvent((data) => {
logEvent({ data, theme: currentTheme }); // reads latest theme without being a dep
});
useEffect(() => {
const socket = connect(userId);
socket.on('event', onEvent); // stable reference, but always reads latest theme
return () => socket.disconnect();
}, [userId]); // only reconnects when userId changes
}
```
---
## `<Activity>`: Background Preloading and State Preservation
`<Activity>` (React 19.2) renders hidden UI — preloading data, CSS, and components for the next navigation destination without impacting visible performance. It also preserves form state when users navigate away.
```jsx
import { Activity } from 'react';
function App({ currentRoute }) {
return (
<>
{/* Visible content */}
<Activity mode={currentRoute === 'home' ? 'visible' : 'hidden'}>
<HomePage />
</Activity>
{/* Preloaded but hidden — data fetches run in background */}
<Activity mode={currentRoute === 'orders' ? 'visible' : 'hidden'}>
<OrdersPage />
</Activity>
</>
);
}
```
**`mode` values:**
- `"visible"`: Rendered and in the DOM
- `"hidden"`: Rendered but hidden — state preserved, data prefetched, not painted
Use `<Activity>` for navigation destinations the user is likely to visit next. Avoid wrapping large, rarely-visited pages — the background renders cost CPU.
---
## `<ViewTransition>`: Coordinated Animations (React 19.2 experimental)
`<ViewTransition>` integrates React's rendering with the browser's native View Transition API for smooth page-to-page animations.
```jsx
import { ViewTransition } from 'react';
import { startTransition } from 'react';
function NavLink({ href, children }) {
return (
<a
href={href}
onClick={(e) => {
e.preventDefault();
startTransition(() => {
navigate(href); // Your router's navigate function
});
}}
>
{children}
</a>
);
}
// Wrap the animated element
function PageContent({ route }) {
return (
<ViewTransition>
<div key={route}>
{/* Content changes trigger view transition animation */}
<RouteContent route={route} />
</div>
</ViewTransition>
);
}
```
The view transition only fires when the state update is wrapped in `startTransition`. Non-transition updates render immediately without animation.
---
## Resource Preloading APIs
React 19 added `react-dom` APIs for preloading resources before they're requested, reducing waterfall latency:
```jsx
import { preload, preinit, prefetchDNS, preconnect } from 'react-dom';
function App() {
// Preload font — triggers <link rel="preload"> immediately
preload('/fonts/Inter.woff2', { as: 'font', crossOrigin: 'anonymous' });
// Preconnect to external API — establishes TCP+TLS before first request
preconnect('https://api.stripe.com');
// Prefetch DNS — faster than nothing, cheaper than preconnect
prefetchDNS('https://cdn.analytics.com');
// Preinit a script — download AND execute immediately
preinit('/scripts/analytics.js', { as: 'script' });
return <App />;
}
```
These calls are **idempotent** — safe to call multiple times; the browser deduplicates. Call them as early as possible in the tree.
---
## Common Performance Mistakes
| Mistake | Symptom | Fix |
|---------|---------|-----|
| Manual `useMemo`/`useCallback` everywhere in React 19 | Over-memoization, harder-to-read code | Let the Compiler handle it; only add manual memo for external stability requirements |
| Expensive computation in render (no memo) | High `baseDuration` in Profiler | `useMemo` for pure computations; `startTransition` for deferred renders |
| State updates for urgent and non-urgent work combined | Typing feels sluggish | Split urgent updates (typing) from non-urgent (filtering results) with `useTransition` |
| New object/array literals in JSX props | Child re-renders every time | Move stable data outside component or use the Compiler; avoid `{}` inline |
| `useEffect` deps include theme/locale | Effect fires too often | `useEffectEvent` for values that are read but don't trigger reconnects |
| No `<Activity>` on predictable navigation destinations | Visible load flash on navigation | Wrap likely-next pages in `<Activity mode="hidden">` |
| Missing `key` on list items | React reconciles wrong elements | Always provide stable, unique keys — never array index for sortable/filterable lists |
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!