About react-performance
A React skill for advising on new code and reviewing changes through performance and pattern-focused checks. It detects the project’s React version and libraries, then reports applicable findings with locations and concrete fixes.
Key features
- 01Advisor and reviewer modes
- 02Checks unnecessary re-renders and memoization
- 03Inspects Context, refs, and stale closures
- 04Reviews async fetching and race conditions
- 05Accounts for React version and project libraries
Use cases
- 01Choose component structure before writing React code
- 02Review changes for avoidable renders
- 03Find stale dependencies or closure issues
- 04Check fetch logic for waterfalls and races
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ robsonrung/ rar-skills --skill react-performanceAfter installing, type/react-performancein Claude Code to confirm it loaded.
Bundled filesreferences/cheatsheet.md
Details
From the project READMEReact Performance & Patterns Lens
Advise and review React code using the rules from Advanced React by Nadia Makarevich. It asks one set of questions: will this re-render more than it needs to, is memoization actually doing anything, is this closure stale, and is the async path safe? Not a bug hunt for logic errors — use a code-review pass for that.
Two modes, pick by context:
- Advisor (while writing new React code): steer the decision before the code is
written — prefer composition over
memo, place state correctly, structure the fetch. - Reviewer (on existing/changed code): audit against the checks below, report findings
grouped by lens. Cite
file:line, name the rule, propose the concrete fix. Skip lenses that don't apply rather than padding. If a component is clean, say so plainly.
Detect the target first (read before applying)
Every lens below applies from React 17 onward. What differs across versions is a handful of specific rules — so detect, never assume:
- React version. Read the target repo's
package.json(react/react-dom, and the lockfile if the range is loose). Note the major. - State library. Look for an external store — Redux Toolkit, Zustand, Jotai, MobX, TanStack Query, SWR, Apollo — and whether it already owns server state.
- UI library. Look for MUI, Chakra, Radix, Headless UI, Ant, shadcn/ui, Tailwind-only, or hand-rolled components.
Then apply the lenses against what you found, and flag version-specific rules explicitly rather than asserting one version's behavior:
- Refs and
forwardRef. Before React 19,forwardRefis required for a function component to receive aref. From React 19,refis a regular prop andforwardRefis no longer needed. Check the detected version before flagging either shape as wrong. - Automatic batching. React 17 batches only inside React event handlers; React 18+ batches everywhere. A "why did this render twice" finding depends on which one you are in.
- Say which version a finding assumes: "on the detected React 17, …" beats a bare rule.
Two detection-driven judgments that recur:
- Deep, re-render-sensitive component trees (styled-component-heavy libraries such as MUI are the common case) make unnecessary parent re-renders expensive — the cascade runs through every styled wrapper. Weight Lens 1 higher when you detect one.
- When an external store with memoized selectors already exists, reach for its selectors over a hand-rolled Context for cross-tree shared state; keep Context for low-frequency, localized config (theme, current entity), not hot state. When no such store exists, the Context guidance in Lens 3 is the whole answer.
The book (2023) matches React 17/18 closely; where the detected version is newer, prefer the detected framework's documented behavior over the book's assumption.
The golden rule (applies to every lens below)
Composition first, memoization last.
React.memois the last resort after composition techniques have failed — because memoizing all props correctly is harder than it looks, and one missed non-primitive prop silently defeats it.
Lens 1 — Unnecessary re-renders (ch 1–4)
A state update re-renders the owning component and all of its children, regardless of
props. Props only matter once React.memo is involved. So the cheapest fix is structural,
not memoization.
Check, in order:
- Move state down. Is fast-changing state (an input value, a hover flag, an open/closed toggle) held high in the tree, forcing a big subtree to re-render? Extract the state + the small piece that uses it into a child component. This is the single highest-value fix in the book.
- Children / elements as props. When a component owns state but renders a heavy
subtree that doesn't depend on that state, pass the subtree as
children(or as an element prop). Elements passed in as props don't re-render when the parent's own state changes — they were created by the parent's parent. Exact pattern: cheatsheet ch 2. - Render props only when the children genuinely need the parent's state/DOM data (e.g. logic attached to a DOM element). Hooks replaced ~99% of the old "share stateful logic" use case — don't reach for render props just to share logic.
Red flags: a top-level page component holding an input's value; a useState in a provider
or layout that wraps half the app; "I'll wrap it in memo" as the first idea.
Lens 2 — Memoization that does nothing (ch 5)
React compares objects/arrays/functions by reference, in memo props and in hook deps.
useMemo memoizes a result; useCallback memoizes the function itself.
Flag memoization that buys nothing — useMemo/useCallback is only justified when one
of these holds:
- the value is a dependency of another hook (
useEffect/useMemo/useCallback), or - the value is a prop passed to a component wrapped in
React.memo, or - it's passed down to a component that then hits one of the above.
And flag React.memo that is silently defeated:
- a non-primitive prop (object/array/function/
children) passed to the memo'd component is recreated each render → memo is useless. All props must be primitive or stable.childrenis a prop too — memoizing it is easy to forget. - the value came from another non-memoized prop or hook result → memoization chain is broken.
Rule of thumb: if you're memoizing a prop, prove the consumer is React.memo'd or uses it
as a dep. Otherwise delete the memo.
Memo'd list items, or state mysteriously resetting/persisting across renders → keys & reconciliation, cheatsheet ch 6.
Lens 3 — Context performance (ch 8)
Every consumer of a Context re-renders when the provider value changes — and standard
memoization can't stop it.
- Flag a provider whose
value={{...}}/value={[a, b]}is an unmemoized inline object/array — every parent render forces every consumer to re-render. Wrap it inuseMemo(and callbacks inuseCallback). - For multiple unrelated values, split into multiple providers so a change to one
doesn't re-render consumers of the other.
useState→useReducerhelps keep the data and the API in separate stable contexts. - No real selectors exist for Context; you can fake them with
React.memo+ HOCs (see cheatsheet ch 7), but if you find yourself doing that, reach for the external store the repo already has (per detection above) instead — a real selector API beats a faked one.
Lens 4 — Refs, closures & stale state (ch 9–11)
- A ref is a mutable box preserved across renders; mutating
ref.currentdoes not re-render and is synchronous. Use it for values that must persist but shouldn't trigger renders (timers, latest-callback, previous value, DOM nodes). Don't use it for anything that should appear in the UI. forwardRefis required before React 19 to pass arefprop to a function component; from React 19refis a regular prop. Check the detected version before flagging. Either way, expose a controlled imperative API withuseImperativeHandlerather than leaking the DOM node.- Stale closure = the #1 bug here. A function created in render "freezes" the state and
props it closed over. If it's memoized (
useCallback/useMemo) with a missing dep, or stored in a ref once, it keeps reading old values.- Detect: a
useCallback/useEffect/useMemoreading state/props but missing them from deps; a callback stored inref.currentthat's never refreshed. - Escape: refresh the closed function in
ref.currentinside auseEffecton every render, then callref.current()— it always sees the latest data. This is also how you keep aReact.memochild stable while still calling fresh logic.
- Detect: a
Lens 5 — Debounce / throttle (ch 11)
debounce/throttle only work if the same instance lives across the component's life.
- Flag
debounce(...)/throttle(...)/setTimeoutid created inline in render or in a non-memoized handler — the timer is recreated every render and never fires correctly. - Fix: memoize the debounced function once (
useMemo/ref), and access the latest state via the ref-refresh trap from Lens 4 (a naive memo freezes state at creation time).
Lens 6 — Flickering UI / useLayoutEffect (ch 12)
Measure-then-mutate DOM work (size/position) in useEffect lets the browser paint the
"before" frame first → visible glitch. Use useLayoutEffect — it runs synchronously
before paint. SSR caveat and exact pattern: cheatsheet ch 12.
Lens 7 — Portals & stacking context (ch 13)
Modals/tooltips/dropdowns clipped by an ancestor's overflow or trapped in a Stacking
Context (nothing escapes one, not even position: fixed) → render via a Portal.
Most component libraries' overlay primitives already portal (MUI Modal/Popper, Radix,
Headless UI) — check the detected library, then flag hand-rolled overlays that don't. CSS
rules and event-bubbling behavior: cheatsheet ch 13.
Lens 8 — Data fetching: waterfalls & race conditions (ch 14–15)
- Waterfall: sequential/conditional
awaits that could be parallel. Flag dependentuseEffectfetches andawait a; await bwhereaandbare independent → usePromise.all, parallel promises, or a data-provider Context. Mind browser parallel request limits; critical resources can be prefetched before React mounts. - Race condition:
setStateafter anawait/.thenin an effect keyed on a changing value (e.g.url, an id). A slow earlier request can resolve after a newer one and overwrite fresh data. Fixes (prefer the last two): compare the resolved id beforesetState; cleanup-flag inuseEffect;AbortController. Exact patterns: cheatsheet ch 15. (A server-state data layer — RTK Query, TanStack Query, SWR, Apollo — handles this for you; when the repo has one, prefer it over hand-rolledfetchin effects for server state.)
Lens 9 — Error handling (ch 16)
After React 16, an uncaught render error unmounts the whole app — a few
ErrorBoundarys in strategic places are non-negotiable. Boundaries miss
callbacks/setTimeout/promises; try/catch misses render/useEffect/nested components.
Bridge by re-throwing async errors into the render lifecycle, or use the
react-error-boundary library. Exact hook and coverage matrix: cheatsheet ch 16.
How to apply
- Identify which lenses the change touches (a fetch hook → 8; a provider → 3; a memo'd component → 1, 2, 4).
- In advisor mode, recommend the structural option first (move state down / composition) and only escalate to memoization when the checks in Lens 2 are met.
- In reviewer mode, report per lens:
file:line→ which rule → concrete fix. - Don't invent re-renders that don't matter — a component that renders cheaply and rarely needs no optimization. Optimize where state is hot and the subtree is heavy.
See references/cheatsheet.md for the per-chapter decision tables and code snippets.