Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
The React team's core belief: Don't optimize what you haven't measured. Most components don't need memoization. The ones that do need it applied correctly, or it's worse than nothing.
The Foundational Principle
"Premature optimization is the root of all evil, but missing obvious optimization is the root of all jank."
React performance: fewer renders, less JavaScript shipped, only visible rows rendered, always measured first.
Core Principles
1. React.memo: The Three-Part Decision
React.memo skips re-rendering when props haven't changed. It only helps when ALL three are true: same props frequently, expensive render, and stable prop identity.
// String concat is not expensive. Just compute it.const fullName = useMemo(() =>`${first}${last}`, [first, last]);
// This:const fullName = `${first}${last}`;
Dependency array pitfall:
// BUG: options is a new object every render, useMemo never cachesconst options = { threshold: 10, limit: 100 };
const result = useMemo(() =>processItems(items, options), [items, options]);
// FIX: inline the values so the dependency is stableconst result = useMemo(() =>processItems(items, { threshold: 10, limit: 100 }), [items]);
3. useCallback: Stable Function References
useCallback only matters when the callback is passed to a React.memo component.
Not this -- useCallback without a memoized consumer:
The relationship: useCallback is the supplier, React.memo is the consumer. One without the other does nothing.
4. React Compiler: Auto-Memoization
React Compiler (formerly React Forget) inserts memoization at build time, making manual memo largely unnecessary.
// You write (no manual memoization):functionProductList({ products, onSelect }: ProductListProps) {
const sorted = products.toSorted((a, b) => a.price - b.price);
return (
<ul>
{sorted.map(p => <ProductCardkey={p.id}product={p}onSelect={onSelect} />)}
</ul>
);
}
// Compiler auto-memoizes sorted, the map output, and ProductCard props
What to do today: new projects on React 19+ enable the compiler; existing projects keep manual memo until migration; either way follow Rules of React (pure render, stable hooks) so the compiler can optimize.
Windowing: ~20 DOM nodes exist regardless of list length. 10,000 items? Still ~20 nodes.
7. Keys and Reconciliation
Not this -- index keys on a dynamic list:
{todos.map((todo, index) => (
// Inserting at top shifts all indexes -- React unmounts/remounts every item<TodoItemkey={index}todo={todo} />
))}
This -- stable unique keys:
{todos.map(todo => (
<TodoItemkey={todo.id}todo={todo} />// React knows exactly which items moved
))}
Index keys are fine for static lists that never reorder and have no component state. Index keys break lists that reorder, filter, insert, or have local state (inputs, toggles).
8. State Colocation: Keep State Close
Every state update re-renders the owning component and all descendants. Push state down.
functionApp() {
return (
<div><Header /><SearchSection /> {/* owns its own query state */}
<Sidebar /><TooltipWrapper /> {/* owns its own open state */}
</div>
);
}
The rule: if only one subtree uses a piece of state, that subtree owns it. Lift only when siblings genuinely share.
9. Children as Props Pattern
Children passed as props are already created -- they skip re-rendering when the parent re-renders.
Why:<ExpensiveTree /> is created in App's render. App doesn't re-render when ColorPicker's state changes, so the element reference is stable.
10. Profiler and DevTools: Measure First
DevTools Profiler workflow: Record > interact > stop > read flame chart. Wide bars = slow renders. Gray = skipped. Focus on components that render often AND take long.
Programmatic Profiler:
import { Profiler, ProfilerOnRenderCallback } from'react';
constonRender: ProfilerOnRenderCallback = (id, phase, actualDuration) => {
if (actualDuration > 16) { // longer than one frame at 60fpsconsole.warn(`Slow render: ${id} took ${actualDuration.toFixed(1)}ms`);
}
};
functionApp() {
return (
<Profilerid="Navigation"onRender={onRender}><Navigation /></Profiler>
);
}
The optimization loop: Profile > rendering too often? React.memo + stable props. Each render slow? useMemo or virtualize. Re-profile to verify.
Anti-Patterns
Memo everything -- comparison overhead without benefit. Memoize selectively.
Unstable keys -- Math.random() or index on dynamic lists forces full DOM recreation.
Inline objects in JSX -- style={{ color: 'red' }} defeats React.memo with a new reference every render.
State too high -- top-level state re-renders the entire tree on every change.
Giant context values -- one context with many fields re-renders all consumers on any change. Split by update frequency.
useEffect for derived state -- filtering in useEffect + setState causes double renders. Compute during render.
Is the component visibly slow when profiled?
No -> Don't memoize.
Yes -> Rendering too often with same props?
Yes -> React.memo + stabilize props (useCallback/useMemo)
No -> Each render expensive?
Yes -> useMemo the expensive part, or virtualize
No -> Problem is elsewhere. Check parent.
Should I Code-Split?
Target
Action
Route/page
Always split
Heavy component (editor, chart, map)
Split with lazy + Suspense
Small UI element
Keep in main bundle
Code Review Checklist
No React.memo without a measured performance problem
useCallback only when passed to memoized children
useMemo only for referential stability or expensive computation
Dynamic lists use stable unique keys (not index)
Long lists (100+ items) virtualized or paginated
Routes code-split with React.lazy
State lives in the lowest component that needs it
No inline objects/arrays passed to memoized components
No derived state in useState + useEffect
Performance claims backed by Profiler measurements