React Render Profiler
Find and fix wasteful React re-renders by classifying the cause — unstable prop/callback/object identities, context value churn, state lifted too high, expensive work in render, or unvirtualized lists — confirming it with a measurement, then applying the one targeted fix and re-measuring. Use when a React UI is janky, slow to type in, or re-renders far more than the data actually changed.
npx agentscamp add skills/react-render-profilerInstall to ~/.claude/skills/react-render-profiler/SKILL.md
Slow React UIs are usually re-render problems, and the fix is rarely "add useMemo everywhere." This skill classifies the actual cause, proves it with the Profiler or why-did-you-render before touching code, applies the one targeted fix — colocate state, stabilize identity, split context, memo, or virtualize — and re-measures to confirm the render count dropped.
A janky React UI is almost always re-rendering more than the data changed — and the reflex fix, wrapping everything in useMemo/memo, usually adds cost and complexity without helping, because it doesn't address why the component re-rendered. This skill makes the work diagnostic: name the cause class, prove it with a measurement, apply exactly one matching fix, and re-measure. No blind memoization.
When to use this skill
- Typing in an input is laggy, or interacting with one widget visibly re-renders unrelated parts of the page.
- The React DevTools Profiler shows a component (or a whole subtree) committing on interactions that shouldn't touch it.
- A list or table with hundreds of rows stutters on scroll, filter, or keystroke.
- A
useEffect/useMemoruns every render even though its inputs "look" the same. - You're tempted to sprinkle
memo/useCallbackand want to confirm where they actually pay off first.
Instructions
- Measure before you touch code. Open React DevTools → Profiler, record the slow interaction, and read the flamegraph: which components committed, how many times, and why (enable "Record why each component rendered"). For a sharper signal on a specific component, wire up
@welldone-software/why-did-you-renderin dev and check the console for which prop/state changed identity. Do not edit anything until you have a named culprit and a render count. - Classify the cause — pick exactly one per culprit. (a) Unstable identity: an object/array/function literal created in the parent's render and passed as a prop, so a
memo'd child or an effect dep changes every render. (b) Context churn: a context Provider whosevalue={{...}}is a fresh object each render, re-rendering every consumer. (c) State too high: state lives in an ancestor, so a localized change re-renders a large subtree. (d) Expensive render work: heavy compute (sorting/formatting/parsing) runs inline in render. (e) Unvirtualized long list: hundreds/thousands of DOM rows all committing. - Fix (c) by moving state, not memoizing. If a keystroke or toggle re-renders a big subtree, colocate the state into the smallest component that uses it, or lift it down into a child. Moving state is the cheapest, most durable fix and often deletes the need for any
memoat all — try this before reaching for memoization. - Fix (a) by stabilizing identity at the source. Wrap callbacks passed to memoized children in
useCallback, and derived objects/arrays inuseMemo, with honest dependency arrays. This only helps if the child is memoized (React.memo) or the value is an effect/memo dependency — stabilizing a prop to an unmemoized child does nothing. - Fix (b) by splitting or memoizing context. Memoize the Provider
valuewithuseMemo, and split a single fat context into separate contexts (e.g. state vs. dispatch, or per-concern) so a consumer only re-renders when the slice it reads changes. - Fix (d) by memoizing the computation or moving it out. Wrap the expensive calculation in
useMemokeyed on its real inputs, or hoist it out of render (precompute, server-side, oruseDeferredValuefor low-priority work). Memoize the work, not the component. - Fix (e) by virtualizing. Render only visible rows with
@tanstack/react-virtual(orreact-window);memoon the row component matters here because virtualization recycles rows. - Re-measure and report the delta. Re-record the same interaction in the Profiler and capture the new render count per culprit. If the count didn't drop, you classified the cause wrong — revert the change (don't leave a
memothat bought nothing) and go back to step 2.
WARNING
Blanket memoization is a regression, not a fix. memo/useMemo/useCallback each cost a comparison and retained memory every render, add dependency-array bugs, and break the moment one prop's identity still churns. Never add them without a Profiler reading showing they remove a real render — and when the true cause is class (c), moving state deletes the problem while memoization only masks it.
NOTE
React.memo compares props shallowly, so it is defeated by a single unstable prop (an inline style={{...}}, onClick={() => ...}, or data={[...]}). A memo'd child that still re-renders on every parent commit is the signature of an unstable-identity prop (cause a) — not a reason to remove the memo.
Output
Per culprit: the component name, the measured cause class with the evidence (Profiler "why it rendered" reason or why-did-you-render line), the single targeted fix as an Edit diff, and before/after render counts for the same recorded interaction. End with a one-line verdict per fix (kept / reverted-no-effect) so no no-op memoization is left behind.
Related
- Bundle AnalyzerAnalyze a JS/TS production bundle and surface the biggest size wins — heavy dependencies, duplicate packages, missing code-splitting, oversized polyfills, and dev/server code leaking into the client. Use when a bundle is too large and you need a ranked, actionable reduction plan.
- Dead Code FinderFind genuinely unused code — unreferenced exports, unreachable files, and unused dependencies — and remove it safely with build/test verification. Use when trimming a codebase or untangling years of accreted cruft.
- Type Coverage ImproverRaise TypeScript type strictness incrementally — measure the any/implicit-any baseline, enable one strict sub-flag at a time, and fix the fallout per flag instead of all at once, keeping the typecheck green at every step. Use when a codebase is loosely typed, when you want strict mode on without a big-bang break, or when `any` keeps hiding bugs that surface in production.
- Flamegraph AnalyzerTurn a CPU profile or flamegraph into a concrete optimization instead of guessing where the time goes: capture under a realistic workload with a sampling profiler, read the graph correctly (width = time, depth ≠ time), find the widest self-time leaves, ask if that work is necessary/redundant/algorithmically wrong, fix the biggest contributor, then re-profile. Use when code is CPU-bound and slow, a function is hot but you don't know which part, or you have a profile you can't interpret.
- Web Vitals OptimizerDiagnose and fix Core Web Vitals — LCP, CLS, and INP — by treating real-user field data at p75 as the source of truth, using Lighthouse/WebPageTest only to find the at-fault element, script, or shift, then applying the one targeted fix per metric and re-measuring. Use when a page feels slow, scores poorly on PageSpeed/Lighthouse, or fails CWV in CrUX/RUM field data.