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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- react-render-profiling Installs to ~/.agents/skills/react-render-profiling/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install react-render-profiling The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project react-render-profiling Installs to .agents/skills/react-render-profiling/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/react-render-profiling.zipunzip -o react-render-profiling.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) react-render-profiling PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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
- You are planning React Compiler adoption or an app-wide memoization strategy: use react-performance
- The app is React Native and the lag is on device: use react-native-performance
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.
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
- Read
AGENTS.mdandpackage.json: React version, framework (Next.js, Vite, React Router), state library, whether React Compiler (babel-plugin-react-compiler) is on. - Reproduce the slowness and write it down as a script: “type 5 characters in the search box on /orders with 2,000 rows”.
- 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.
| Need | Build |
|---|---|
| Which components render and why | Dev build is fine |
| Real timings | Profiling 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
memoboundary 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 shows | Cause | Fix |
|---|---|---|
| Whole page re-renders on each keystroke | Input state lives in a top-level component | Move 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 everything | useMemo the value; split into state and dispatch contexts, or by concern |
| Memoised child re-renders, “props changed: style/onClick” | Inline object, array or function props | Hoist constants; useCallback/useMemo only for props crossing a memo boundary |
| One component has large self time | Heavy 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 mount | Long list rendered in full | Virtualise (TanStack Virtual, react-window); paginate |
| Components unmount and remount | Unstable key (random, index on reordered list) or component defined inside another component | Stable IDs as keys; define components at module level |
| Commit is fine but input lags | Urgent update blocked behind a slow non-urgent one | startTransition for the expensive update, or useDeferredValue for the derived list |
| Store subscribers all re-render | Selector returns a new object, or subscribes to whole store | Narrow 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
memoeverywhere 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.yaml
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.