Performance No. 305

React Re-renders skill for Claude Code

Profile first, memoize last. Claude profiles the slow interaction in a production build, names the components whose commits dominate and why each rendered, then fixes structure first: moving state down, passing children, splitting context, using store selectors. Memoization, transitions and virtualization come only where the profiler shows they pay off, and React Compiler adoption follows its lint rules.

  • namereact-rerenders
  • size199 lines · ≈2,300 tokens
  • files1
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Finds and fixes React rendering performance problems with evidence — React DevTools Profiler and render highlighting, the Profiler API, and React Scan — then applies the cheapest structural fix first (state colocation, splitting context, stable keys, selectors), memo/useMemo/useCallback only where measured, React Compiler adoption, transitions, and list virtualization. Use when a React UI is laggy while typing or scrolling, a component re-renders too often, INP is poor on a React page, deciding whether to add memoization, or enabling React Compiler.

Try asking

  • Typing in this search box lags badly — profile it and fix the re-renders
  • Should we wrap these components in memo, or is something else wrong?
  • Help me enable React Compiler in our Next.js app and check it is working

Use it when

  • A React UI lags while typing, scrolling, or opening panels
  • The profiler shows long commits or huge render counts
  • INP is poor on a React-rendered page
  • Evaluating or rolling out React Compiler

Skip it when

How to check the result

The same interaction re-profiled under the same throttling shows a shorter slowest commit and fewer renders of the hot component, with tests still passing.

The common mistake it prevents

Wrapping components in memo while still passing new inline objects or callbacks each render, which adds comparison cost and prevents nothing.

~/.claude/skills/react-rerenders/SKILL.md

--- name: react-rerenders description: Finds and fixes React rendering performance problems with evidence — React DevTools Profiler and render highlighting, the Profiler API, and… license: MIT ---

Done well means: the slow interaction is identified and profiled, the components that render unnecessarily or expensively are named with their render counts and durations, the fix is structural where possible, memoization is added only where the profiler proves it helps, and the same profile afterwards shows the commit time dropped.

Ground rules

  • Re-renders are not bugs. Slow commits are. A component rendering 50 times in 1 ms total is fine; one rendering once for 120 ms is the problem.
  • Profile a production build (or the profiling build) for timings. Development mode, StrictMode double-rendering, and DevTools overhead distort numbers.
  • Fix structure before memoizing: move state down, lift content up, split contexts. These make memo unnecessary and survive refactors.
  • If the project uses React Compiler, do not add manual useMemo/useCallback/memo for new code; check why the compiler skipped a component instead.
  • Never “fix” a render problem by breaking correctness (stale closures, missing deps, mutating state to avoid renders).

Workflow

- [ ] 1. Name the slow interaction (typing in X, opening Y, scrolling Z) and measure it
- [ ] 2. Profile it: which commits are slow, which components dominate each commit
- [ ] 3. For each hot component: why did it render? (props, state, context, parent)
- [ ] 4. Apply the cheapest fix from the table below
- [ ] 5. Re-profile the same interaction; compare commit durations
- [ ] 6. Check correctness: behavior, effects, and tests unchanged

1–2. Measure and profile

  • React DevTools > Profiler: enable “Record why each component rendered while profiling”, record the interaction, sort the ranked chart by self time. Settings > “Highlight updates when components render” for a quick visual of render scope.
  • React Scan (npx react-scan@latest <url> or the script/package in dev) overlays render counts and flags renders caused by unstable props.
  • Chrome DevTools Performance panel with CPU throttling shows whether the time is React rendering, layout, or something else (third-party scripts, JSON parsing). Since React 19.2 the Performance panel can show React-specific tracks; use them if available.
  • In code, measure a subtree:
import { Profiler, type ProfilerOnRenderCallback } from "react";

const onRender: ProfilerOnRenderCallback = (id, phase, actualDuration, baseDuration) => {
  if (actualDuration > 16) console.warn(`[render] ${id} ${phase} ${actualDuration.toFixed(1)}ms (base ${baseDuration.toFixed(1)}ms)`);
};

<Profiler id="ProductGrid" onRender={onRender}><ProductGrid /></Profiler>

actualDuration much lower than baseDuration means memoization is already working; close to it means everything re-renders every time.

3. Why did it render?

CauseProfiler saysTypical fix
Parent rendered“The parent component rendered”Move state down; pass children as props; memo if the child is expensive
Props changed by identity only“Props changed: onClick, style”Stable references (compiler, useCallback, hoisting constants)
Context value changed“Context changed”Split context; memoize the value object; selector-based store
Own state changed“State changed” / hook N changedIs the state needed here? Derive instead of storing
Key changedComponent remounts (mount phase)Stable keys from data IDs, never index for reorderable lists, never Math.random()

4. Fixes, cheapest first

Colocate state. Typing in an input that lives at the page root re-renders the page.

// Before: every keystroke re-renders <ExpensiveList />
function Page() {
  const [query, setQuery] = useState("");
  return (<><input value={query} onChange={(e) => setQuery(e.target.value)} /><ExpensiveList /></>);
}

// After: state lives where it is used
function SearchBox() {
  const [query, setQuery] = useState("");
  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}
function Page() { return (<><SearchBox /><ExpensiveList /></>); }

Lift content up (children as props). A stateful wrapper that receives children does not re-render them when its own state changes, because the elements were created by the parent.

Split contexts. One context with { user, theme, cart, setCart } re-renders every consumer on any change. Split by update frequency, keep setters in a separate context (they are stable), and memoize provider values:

const value = useMemo(() => ({ items, total }), [items, total]);
<CartContext.Provider value={value}>{children}</CartContext.Provider>

For high-frequency shared state, use a store with selectors (Zustand, Redux, Jotai, or useSyncExternalStore) so components subscribe to slices:

const count = useCartStore((s) => s.items.length);              // re-renders only when length changes
const [a, b] = useCartStore(useShallow((s) => [s.a, s.b]));     // multiple fields, shallow compare

Derive, do not sync. useEffect(() => setFiltered(filter(items)), [items]) causes an extra render; compute during render (memoize only if expensive).

Memoize where measured.

ToolUse when
memo(Component)Component is expensive and often receives the same props while its parent re-renders
useMemoA calculation takes > ~1 ms (measure with console.time) or a value must be referentially stable for a memoized child/effect deps
useCallbackA function is passed to a memoized child or used in effect deps

memo is useless if any prop is a new object/array/function each render; fix all of them or none.

Keep the UI responsive during heavy updates.

const [isPending, startTransition] = useTransition();
function onFilterChange(next: Filter) {
  setFilter(next);                                 // urgent: input reflects immediately
  startTransition(() => setResultsFilter(next));   // non-urgent: can be interrupted
}

const deferredQuery = useDeferredValue(query);     // list renders with the lagging value
const results = useMemo(() => search(items, deferredQuery), [items, deferredQuery]);

Virtualize long lists. Rendering more than a few hundred rows of non-trivial components is slow regardless of memoization. Use TanStack Virtual (@tanstack/react-virtual) or react-window; or CSS content-visibility: auto for simpler long pages.

React Compiler

React Compiler (stable since v1.0, October 2025) auto-memoizes components and hooks at build time, making most manual memo/useMemo/useCallback unnecessary.

Adoption checklist:

- [ ] React 19 (or 17/18 with the react-compiler-runtime package)
- [ ] eslint-plugin-react-hooks with the compiler-related rules enabled; fix violations first
- [ ] Add babel-plugin-react-compiler (or the framework option: Next.js `reactCompiler: true`,
      Vite via @vitejs/plugin-react babel plugins, Expo per its docs)
- [ ] Roll out incrementally if needed (compilationMode / directory filters), measure, expand
- [ ] Keep existing manual memoization until profiling shows it is redundant; then remove
  • The compiler skips components that break the Rules of React (mutating props/state, reading refs during render, conditional hooks). ESLint violations are the list of what it could not optimize.
  • "use no memo" at the top of a component opts it out temporarily while debugging.
  • Verify in React DevTools: compiled components carry a “Memo” badge next to their name.

Gotchas

  • Inline objects in props (style={{...}}, options={{...}}) defeat memo children; hoist constants out of the component.
  • Context providers whose value is a fresh object re-render every consumer on every parent render.
  • Index keys on lists that reorder or insert at the top remount and mis-associate state.
  • useEffect chains that set state on mount cause double renders and layout flashes; derive or move to event handlers.
  • Expensive work in render that is not React’s fault (sorting 50k rows, date parsing) — move to useMemo, a worker, or the server.
  • React Query / SWR: select only what you need (select option) to avoid re-renders on unrelated field changes.
  • Lists inside components that also hold input state: typing re-renders the list. Split.

Verification

  1. Re-record the same interaction in the Profiler under the same conditions; compare the slowest commit duration and the hot component’s render count.
  2. For INP-related work, confirm with the DevTools Performance panel that the interaction has no long task over 50 ms before the next paint.
  3. Run the test suite; check that effects still fire when they should (memoization can hide missing dependencies).

Report template

**Interaction:** typing in the product filter (6x CPU throttle, production build)
**Before:** slowest commit 142 ms; <ProductGrid> re-rendered on every keystroke (600 cards)
**Cause:** filter text state lived in <CatalogPage>; ProductCard received inline `style` prop
**Fix:** moved query state into <FilterBar>, deferred grid filter with useDeferredValue, virtualized grid
**After:** slowest commit 11 ms; ProductCard renders only for visible rows
**Memo added:** none (structural fixes sufficient)

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc