React Render Profiling skill for Codex

Record the lag, read why it rendered, fix that. Codex records the exact interaction that feels slow, reads the commit chart, ranked chart and why-did-this-render reasons in React DevTools, and cross-checks with the Chrome Performance panel's React tracks. Each slow component maps to a cause such as state placed too high, an unstable context value, an unvirtualised list or a remounting key, and the fix is proven with a second recording of the same script.

  • Performance
  • $react-render-profiling
  • 135 lines
  • 2 files
  • ~1,750 tokens
  • MIT
  • Updated

Install

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

Installs to ~/.agents/skills/react-render-profiling/ 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-render-profiling, 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

Profiles slow React renders with the React DevTools Profiler (flamegraph, ranked chart, why-did-this-render), the Chrome Performance panel React tracks, and the Profiler component's onRender timings, then fixes the specific cause - state placed too high, unstable context values or props, expensive render work, unvirtualised lists, remounting keys or urgent updates that should be transitions. Use when typing, filtering, scrolling or opening a panel in a React app feels laggy, INP is poor on a React page, or someone asks which components re-render and why.

Use it when

  • A React interaction such as typing, filtering or toggling feels laggy
  • INP attribution points at a React event handler or component update
  • You need to know which components render on a change and why

Use something else when

How to check the result

Re-record the same scripted interaction on the same build: the slowest commit time and unrelated renders drop, and the Performance panel shows a shorter interaction task.

The mistake it prevents

Wrapping every component in memo before profiling, which adds comparison cost and leaves the real cause, usually state placement or an unstable context value, untouched.

~/.agents/skills/react-render-profiling/SKILL.md

--- name: react-render-profiling description: Profiles slow React renders with the React DevTools Profiler (flamegraph, ranked chart, why-did-this-render), the Chrome Performance panel React… license: MIT ---

Done well means: the slow interaction is recorded, the commits that make it slow are identified with their components and the reason each rendered, the fix targets that reason, and a second recording shows the commit time dropped. No speculative memo sprinkled across the tree.

Before profiling

  1. Read AGENTS.md and package.json: React version, framework (Next.js, Vite, React Router), state library, whether React Compiler (babel-plugin-react-compiler) is on.
  2. Reproduce the slowness and write it down as a script: “type 5 characters in the search box on /orders with 2,000 rows”.
  3. Use a production-like build when numbers matter. Dev builds are several times slower and double-invoke in StrictMode; use them to find what renders, a profiling build to measure how long.
NeedBuild
Which components render and whyDev build is fine
Real timingsProfiling build: alias react-dom/client to react-dom/profiling; Next.js next build --profile
Field timings from users<Profiler onRender> in a profiling build, or INP attribution via $rum-monitoring

Workflow

- [ ] 1. Enable DevTools Profiler setting "Record why each component rendered while profiling"
- [ ] 2. Record the scripted interaction only (start, act, stop)
- [ ] 3. Find the slowest commits in the commit bar chart
- [ ] 4. In each, read the ranked chart: top self-time components and their render reasons
- [ ] 5. Cross-check in the Chrome Performance panel with CPU throttling
- [ ] 6. Apply one fix per cause; re-record the same script; compare commit durations

Reading the Profiler

  • Commit bar chart (top right): one bar per commit; height and colour = duration. Many medium commits during typing means re-rendering on every keystroke; one huge commit means a single expensive update.
  • Flamegraph: grey = did not render. A wide coloured subtree under a component that should not care about the change is the smell.
  • Ranked chart: components sorted by self time in that commit. Start at the top.
  • “Why did this render?” per component: props changed (which), state changed, hooks changed (which index), context changed, or parent rendered. “Parent rendered” with no prop changes means a memo boundary would skip it; “props changed: onSelect” on every commit means an unstable callback.

Turn on “Highlight updates when components render” (Components tab settings) for a quick visual of render scope before recording.

Chrome Performance panel

React 19.2+ adds React tracks (Scheduler and Components) to the Performance panel in dev and profiling builds, so React work lines up with long tasks, layout and paint. Record with 4x CPU throttling. If the long task is mostly “Recalculate style” or “Layout”, the problem is DOM size or CSS, not React rendering.

Programmatic timings

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="OrdersTable" onRender={onRender}><OrdersTable rows={rows} /></Profiler>

actualDuration well below baseDuration means memoization is already skipping work; close to it means everything re-renders.

Cause -> fix

Profiler showsCauseFix
Whole page re-renders on each keystrokeInput state lives in a top-level componentMove state down into the component that uses it; pass static subtrees as children
Consumers re-render on unrelated changes, reason “context changed”Provider value is a new object each render, or one context holds everythinguseMemo the value; split into state and dispatch contexts, or by concern
Memoised child re-renders, “props changed: style/onClick”Inline object, array or function propsHoist constants; useCallback/useMemo only for props crossing a memo boundary
One component has large self timeHeavy computation in render (sorting, filtering, formatting 10k items)useMemo keyed on inputs, precompute on the server, or move to $web-workers
Thousands of row components mountLong list rendered in fullVirtualise (TanStack Virtual, react-window); paginate
Components unmount and remountUnstable key (random, index on reordered list) or component defined inside another componentStable IDs as keys; define components at module level
Commit is fine but input lagsUrgent update blocked behind a slow non-urgent onestartTransition for the expensive update, or useDeferredValue for the derived list
Store subscribers all re-renderSelector returns a new object, or subscribes to whole storeNarrow selectors; useShallow (Zustand), equality fn or memoised selectors (Redux)
// Keep the input responsive; let the filtered list lag behind
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const visible = useMemo(() => filterRows(rows, deferredQuery), [rows, deferredQuery]);

With React Compiler enabled, components it optimised show a “Memo” badge in DevTools; manual useMemo/useCallback are mostly redundant there. A component that still re-renders under the compiler usually breaks the Rules of React (mutation during render) and was skipped; the compiler’s ESLint rule reports why. Broader memoization strategy and compiler adoption belong to $react-performance.

Hard rules

  • Profile first. Adding memo everywhere costs comparison time and hides the real cause.
  • Measure the same scripted interaction before and after, on the same build type.
  • A fix that drops render time but changes behaviour (stale data from a wrong dependency array) is a bug; run the tests and re-check the UI.
  • Never commit a profiling alias or <Profiler> logging to production paths without the team agreeing; profiling builds are slightly slower.

Verification

  • Re-record: target commit duration and commit count for the script both drop; the flamegraph shows unrelated subtrees grey.
  • Chrome Performance panel at 4x throttle: the interaction’s long task shrinks; for INP-sensitive flows, interaction latency under ~200 ms.
  • Tests and type checks pass.

Report template

## React render profile: <interaction> on <route>
Build: profiling, React <ver>, CPU 4x throttle
Before: 38 commits, slowest 142 ms (OrdersPage tree), INP ~310 ms
After: 38 commits, slowest 21 ms, INP ~120 ms

| Component | Reason it rendered | Fix |
|---|---|---|
| OrdersTable (96 ms self) | parent rendered (search state in OrdersPage) | moved query state into SearchBox; useDeferredValue for filtered rows |
| RowActions x2000 | props changed: onSelect | useCallback in OrdersTable + memo(Row) |
Remaining: <layout cost of 2,000 DOM rows; virtualisation recommended>

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 Render Profiling"
  short_description: "Record the lag, read why it rendered, fix that"
  default_prompt: "$react-render-profiling Typing in the search box on the orders page lags, 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