React Performance skill for Codex

Profile the interaction, then fix the fan-out. Codex starts from one named slow interaction, profiles it in a production build, and classifies the cause as expensive renders or too-wide updates before touching code. It prefers structural fixes such as state colocation, children-as-props, split contexts and virtualization, then adopts the React Compiler incrementally with lint diagnostics and opt-outs, and reports before and after numbers.

  • Frontend
  • $react-performance
  • 182 lines
  • 2 files
  • ~2,150 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- react-performance

Installs to ~/.agents/skills/react-performance/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $react-performance, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Fixes slow React UIs by measuring first with the React DevTools Profiler and Chrome performance tracks, then applying the right fix — React Compiler adoption, state colocation, context splitting, store selectors, useDeferredValue, list virtualization, or targeted memo, useMemo and useCallback. Use when typing or clicking feels laggy, a list re-renders on every keystroke, INP is poor on a React page, when enabling babel-plugin-react-compiler, or when reviewing code full of useless memoization.

Use it when

  • An input, drag or click feels sluggish in a React UI
  • The Profiler shows hundreds of components rendering for a small change
  • Enabling or rolling out babel-plugin-react-compiler
  • Reviewing code for unnecessary or missing memoization

Use something else when

How to check the result

Record the same interaction in a production build before and after: commit duration and the count of rendered components drop, and INP for that interaction is at or under 200 ms.

The mistake it prevents

Measuring in development mode, where Strict Mode double rendering and dev checks make components look several times slower than they are.

~/.agents/skills/react-performance/SKILL.md

--- name: react-performance description: Fixes slow React UIs by measuring first with the React DevTools Profiler and Chrome performance tracks, then applying the right fix — React Compiler… license: MIT ---

Done well means: you can name the interaction that is slow, show its render cost before and after in a profile, and the fix targets the cause (too much work per render, or too many components rendering) rather than sprinkling useMemo across the tree.

Workflow

- [ ] 1. Name the slow interaction and measure it
- [ ] 2. Profile and classify the cause
- [ ] 3. Apply the cheapest structural fix
- [ ] 4. Adopt or tune the React Compiler where it helps
- [ ] 5. Re-measure the same interaction

1. Name and measure

Write the scenario as one line: “Typing in the filter box on /orders with 2,000 rows takes 180 ms per keystroke.” Measure in a production build (npm run build then the framework’s preview/start command). Development builds run Strict Mode double renders and extra checks and can be several times slower.

Targets: interaction-to-next-paint at or under 200 ms at p75 (Core Web Vitals “good”); for a single keystroke, aim for under 50 ms of scripting so input never feels sticky.

2. Profile and classify

React DevTools Profiler, with “Record why each component rendered while profiling” turned on in its settings:

  1. Start recording, perform the interaction once, stop.
  2. Read the flamegraph for the slowest commit. Gray bars did not render.
  3. For each hot component, read “Why did this render?”.

In Chrome DevTools Performance panel, React 19.2+ adds React tracks (Scheduler and Components) in development and profiling builds, which show which priority lane did the work and how long each component took next to long tasks.

Classify:

What the profile showsCauseGo to
One component with a long self timeexpensive render workcompute less, useMemo, move work off render, virtualize
Hundreds of cheap components re-renderingupdate fans out too widecolocate state, split context, selectors, compiler
“Context changed” on many componentsone wide context valuesplit context or use a store with selectors
“Props changed” where values look equalnew object/function identitiescompiler, or stabilize at the source
Long task outside React (layout, style recalc)DOM size or CSSvirtualize, content-visibility, reduce DOM
Render is fast but input still lagsurgent update blocked by slow oneuseDeferredValue or useTransition

3. Structural fixes first

These help with or without the compiler and do not add memo code:

  • Colocate state. Move useState into the smallest component that needs it. A search input’s text state should not live in the page component that renders the grid.
  • Lift content up as children. A component that owns frequently changing state can take the expensive subtree as children; those elements were created by the parent and do not re-render when the child’s state changes.
function ScrollTracker({ children }: { children: React.ReactNode }) {
  const [y, setY] = useState(0);
  useEffect(() => {
    const onScroll = () => setY(window.scrollY);
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => window.removeEventListener("scroll", onScroll);
  }, []);
  return <div data-scrolled={y > 0}>{children}</div>; // children do not re-render on scroll
}
  • Split context. Separate rarely changing values (theme, user) from frequently changing ones (cursor position, form draft). Separate state and dispatch into two contexts so dispatch-only consumers never re-render.
  • Select from stores. With Zustand or Redux, subscribe to the narrowest slice (useStore((s) => s.count)), never the whole state object.
  • Virtualize long lists. Above a few hundred rows of non-trivial markup, render only the visible window with TanStack Virtual or react-window. For long static pages, content-visibility: auto on sections skips off-screen layout and paint.
  • Defer the slow part. Keep the input urgent and let the expensive list lag:
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => filterRows(rows, deferredQuery), [rows, deferredQuery]);
const isStale = query !== deferredQuery;
  • Never define components inside components. A nested function Row() is a new component type each render, so React unmounts and remounts the whole subtree.
  • Keys must be stable IDs. key={index} on a reorderable list or key={Math.random()} forces remounts and loses state.

4. React Compiler

The compiler (stable since 1.0, October 2025) auto-memoizes components and hooks at build time, which removes most hand-written memo, useMemo and useCallback.

Check whether it is already on: look for babel-plugin-react-compiler in package.json and reactCompiler in next.config.*. React DevTools marks compiled components with a “Memo” badge.

Enable (installing needs network; ask for approval in a sandbox without it):

npm install -D babel-plugin-react-compiler@latest
SetupConfiguration
Next.js 16+reactCompiler: true in next.config.ts (Next 15 used experimental.reactCompiler)
Vite + @vitejs/plugin-reactreact({ babel: { plugins: ["babel-plugin-react-compiler"] } })
Babel directlyadd the plugin first in the plugins array
React 17 or 18also install react-compiler-runtime and set the plugin’s target option to "17" or "18"

Rollout rules:

  • Turn on the compiler diagnostics in lint first. They ship in the recommended config of current eslint-plugin-react-hooks; the old standalone eslint-plugin-react-compiler is superseded. Fix rule-of-React violations (mutating props or state, reading refs during render, conditional hooks); the compiler skips components that break them.
  • For a large app, start with compilationMode: "annotation" and add "use memo" to the hottest components, or compile one directory via the plugin’s sources option.
  • Opt a misbehaving component out with a "use no memo" directive at the top of its body, and leave a comment with the reason and an issue link.
  • Do not delete existing useMemo/useCallback in the same change that enables the compiler. Enable, verify, then remove manual memoization in a separate commit, because some of it may guard effect dependencies.

Manual memoization without the compiler

Use it only where the profile points:

  • memo(Component) on a component that re-renders often with equal props and is expensive. It is useless if any prop is a fresh object, array, or inline function.
  • useMemo for a computation that shows up in the profile (sorting or filtering thousands of items), or to keep an object identity stable for a memoized child or an effect dependency.
  • useCallback only when the function is passed to a memoized child or used as an effect dependency.

Wrapping cheap computations in useMemo costs more than it saves and hides the real problem.

Gotchas

  • Profiling in development overstates cost. Confirm in a production or profiling build (react-dom/profiling aliased in the bundler) before claiming a win.
  • Context providers whose value={{ a, b }} is a new object every render re-render every consumer. The compiler stabilizes this; without it, useMemo the value.
  • useEffect that sets state on mount causes a second render of the subtree. Derive the value during render instead when it depends only on props or state.
  • Third-party components that mutate props or read refs in render can break when the compiler memoizes around them. Watch for stale UI after enabling it.
  • A fast React profile with a slow interaction means the cost is elsewhere: layout thrash, huge DOM, synchronous storage reads, or third-party scripts.

Verification

  • Same scenario, same build mode, before and after numbers recorded (commit duration in the Profiler and interaction time in the Performance panel).
  • Components that should not render during the interaction show as gray.
  • Tests and type-check pass; lint has no new rules-of-React errors.
  • For compiler rollouts, click through the main flows looking for stale values.

Output

**Interaction:** typing in Orders filter (2,000 rows), production build, 4x CPU throttle
**Before:** 160 ms commit, 1,940 components rendered, INP 280 ms
**Cause:** filter text state in OrdersPage; every row re-rendered per keystroke
**Fix:** moved state into FilterBar, useDeferredValue for the grid, virtualized rows
**After:** 18 ms commit, 42 components rendered, INP 90 ms
**Follow-ups:** enable React Compiler in annotation mode for /reports

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "React Performance"
  short_description: "Profile the interaction, then fix the fan-out"
  default_prompt: "$react-performance Typing in the search box lags on the orders page; profile it and fix the re-renders"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem