# React Scan

> Install and use react-scan tooling for component-level React render attribution: "set up react-scan", "diagnose React re-renders", "find unnecessary renders", "find unstable props", "measure component renders programmatically", or automated render checks with Playwright (react-scan + playwright, lite headless API for CI). Covers install across Next.js/Vite/Remix/script-tag/browser-extension and the render-attribution → fix → validate loop. For trace-based triage of LCP/INP regressions, ForcedReflow, bundle bloat, or memory leaks, use investigate-web-perf instead.

- Skill: `amhuppert/react-scan` (Agent Skill, multi-file: 10 files)
- Install (CLI): `npx skillmds@latest add amhuppert/react-scan`
- Raw SKILL.md: https://api.skillmd.com/api/skills/amhuppert/react-scan/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: amhuppert (https://skillmd.com/u/amhuppert)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/amhuppert/react-scan

---


# React Scan + Playwright for React Performance Diagnosis

`react-scan` instruments React's fiber tree via the same DevTools hook React DevTools uses, observing every commit without any application code change. It reports **which component rendered, how long it took, and what changed** (props / state / context / hooks / parent cascade). Pair it with Playwright and the result is a deterministic, file:line-attributable workflow for re-render bugs that traces alone cannot resolve.

This skill is the entry point. Detailed setup, integration patterns, API surface, diagnosis classification, and fix recipes live in `references/` and load only when needed.

## When to Use This Skill

Use react-scan when the symptom points at **component-level rendering**, not asset delivery or paint:

- A component re-renders many more times than its props change
- A list/grid feels janky on scroll or input despite small per-frame work
- An interaction (click, keystroke) takes 100–400 ms and the trace shows React render time dominating
- A user reports "everything re-renders when I type"
- A Playwright test needs to assert a render-count invariant (e.g., "search input keystroke causes at most 2 renders of `<Results>`")

**Do not** use react-scan as the first move for: LCP regressions, network/bundle issues, layout shift, third-party scripts. Those belong in `investigate-web-perf` (sibling skill in this plugin). Trace-level analysis with the chrome-devtools-mcp Performance Insights complements react-scan: `investigate-web-perf` is canonical for trace work, `react-scan` is canonical for render-count attribution.

## Modes — Pick One Before Acting

| Mode | When | How |
|---|---|---|
| **Toolbar overlay** (default) | Local dev, human-driven visual debugging | `npx react-scan@latest init`, or CLI proxy, or script tag |
| **Lite (`react-scan/lite`)** | Programmatic data collection, CI, agent-driven analysis | `instrument({ onEvent })` or `instrument({ endpoint })` |
| **CLI proxy** (`npx react-scan@latest URL`) | Quick check of any running app without touching its code | `npx react-scan@latest http://localhost:3000` |
| **Browser extension** | Audit a deployed app you do not control | Install for Chrome/Firefox, click icon |
| **Playwright + injection** | Headless render counting in tests | Inject auto.global.js + patch `onRender` (see `references/playwright-integration.md`) |

For agent-driven workflows the order of preference is:
1. **Lite mode** for structured data extraction (lowest token cost, structured `LiteEvent` records).
2. **Playwright + injection** when needing to drive interactions while collecting renders.
3. **Toolbar overlay** only when a human will look at the screen.

## The Loop

1. **Reproduce deterministically.** Pin the same interaction sequence and the same render-count reset point. React render counts are sensitive to dev/prod, StrictMode (doubled mounts), and concurrent batching — fix these before measuring.
2. **Instrument.** Add react-scan to the app (or inject via Playwright). Confirm `window.__REACT_SCAN__.ReactScanInternals` is present.
3. **Capture.** Trigger the interaction. Collect renders via one of: `onRender(fiber, renders)` callback, lite `subscribe()` events, or `Store.reportData` per-fiber map. Save raw payload to disk; do not stream to model context.
4. **Classify.** For each high-count component, read `changeDescription` (props / state / context / hooks / parent). Cross-reference with source to find the unstable reference. Categories in `references/diagnosis-patterns.md`.
5. **Hypothesize.** One change at a time. Name the specific unstable reference or context boundary.
6. **Fix.** Apply the minimal change (see `references/fix-recipes.md`). Do not bulk-add `React.memo`.
7. **Validate.** Re-run the same Playwright sequence. Compare before/after render counts on the same components. Accept the fix only if the targeted component's count drops by the expected delta AND no other component's count regresses.

## Non-Negotiables

1. **Do not measure react-scan output from a production build by accident.** React Scan no-ops on production builds unless `dangerouslyForceRunInProduction: true` is set. In Playwright against a dev server this works automatically; against any other build, set the flag or you will measure nothing and conclude wrongly. "Production build" here means **React's production build** as detected by bippy's `detectReactBuildType` — not "minified component names." A dev-mode React build can still emit minified single-letter names (e.g., Next.js + Turbopack); counters will work, names will just be unhelpful. If counters stay at zero, suspect prod React; if counters populate but names are `_`/`r`/`i`, you have a minified dev build — fix names via `@react-scan/vite-plugin-react-scan` `autoDisplayNames: true` (Vite) or `babel-plugin-react-display-name` (others) rather than reaching for the prod flag.
2. **Do not measure with React StrictMode enabled in counts you report as "unnecessary renders".** StrictMode double-invokes function components in development. Either disable StrictMode for the measurement, or divide expected counts by 2, or measure interaction-driven renders only (StrictMode doubles mount renders, not update renders).
3. **Do not add `React.memo` without a measured render count first.** Memoization has a non-zero cost; on components that render in <0.1 ms it's net negative. Always measure before and after.
4. **One variable per iteration.** Fixing two unstable refs at once means you cannot attribute the win — and you might fix one while regressing the other.
5. **Use `react-scan/lite` for programmatic capture, not the full bundle.** Lite has no overlay, no Preact, no toolbar — bundle and runtime overhead are minimal.
6. **Save raw events to disk.** A single commit can yield hundreds of fiber events. Reference paths in your report; never paste full `LiteEvent` arrays into the model context.
7. **`react-scan/lite` `keepalive` POSTs cap at ~64 KB.** If using the `endpoint` option for large trees, tune `maxFibersPerCommit` (default 5000) downward or use in-process `subscribe()` instead.

## Quick Decision Tree

- "Set up react-scan in this project" → `references/setup.md`
- "Write a Playwright test that asserts no extra renders" → `references/playwright-integration.md` + `scripts/playwright-render-collector.ts`
- "What does react-scan expose on `window`?" → `references/api-surface.md`
- "Component X re-renders too much — why?" → `references/diagnosis-patterns.md` (load `references/api-surface.md` first if you don't yet have a render payload)
- "I know it's an unstable prop — how do I fix it?" → `references/fix-recipes.md`
- "Walk me through the whole loop end-to-end" → `references/workflow.md`

## Anti-Patterns

- **Reading `onRender` output during JSX render.** The callback fires on every commit; logging on every fire pollutes the console and skews measurements via console overhead. Buffer, then flush after the interaction completes.
- **Counting `onRender` calls per component.** A single commit can produce one `Render[]` array with multiple entries — count the array length, not the callback invocations.
- **Trusting render-count parity between dev and prod.** React skips some scheduler work in production; counts will differ. Validate fixes in the same environment used to measure the problem.
- **Conflating "rendered" with "expensive".** A component can render 100 times per interaction yet contribute <2 ms total. If trace flamegraph attribution is small, drop it and move to the next.
- **Using full `react-scan` in a Playwright test.** The overlay attempts to mount Shadow DOM + Preact, which adds noise to render counts and slows tests. Use `react-scan/lite` or full bundle with `showToolbar: false`.

## References

Load on demand:

- **`references/setup.md`** — Install per framework (Next.js App + Pages, Vite, Remix, script tag, CLI init, Vite plugin, browser extension, lite mode); environment guards and dev/prod gating.
- **`references/playwright-integration.md`** — `addInitScript` injection, the render-counter pattern from react-scan's own E2E suite, lite-mode subscriber, multi-page metrics aggregation, CI gating with budgets, when to disable StrictMode for measurement.
- **`references/api-surface.md`** — `window.__REACT_SCAN__`, `ReactScanInternals`, `Store.reportData`, `onRender(fiber, renders)` shape, `Render` / `ChangeDescription` types, the lite `LiteEvent` taxonomy and `LiteOptions`.
- **`references/diagnosis-patterns.md`** — Classification of root causes from `changeDescription`: unstable props, context over-scope, cascade from parent, memo bypass, hook dependency unstable, store selector instability, child-of-list-without-key.
- **`references/fix-recipes.md`** — Minimal-diff fixes per category: `useMemo` / `useCallback` placement, `React.memo` with custom equality, context split, selector with `shallow`, `useDeferredValue` / `useTransition`, list virtualization, key stabilization, lifting state down.
- **`references/workflow.md`** — End-to-end loop with prompts: reproduce → instrument → capture → classify → fix → validate, plus the slow-interaction (>150 ms long-render frame) and INP-driven variants.
- **`scripts/inject-react-scan.js`** — Playwright `addInitScript`-compatible helper that loads `auto.global.js` and disables the overlay for headless measurement.
- **`scripts/playwright-render-collector.ts`** — Example Playwright test that drives an interaction, collects per-component render counts via the lite subscriber, and writes a JSON report to disk for diff comparison.

