
Crispy Profiling
io.github.edgeorgiev0.1.0Updated Oct 5, 2026
Deterministic React render profiling: renders, wasted renders and their causes per component.
Overview
Profiles React re-renders in headless Chromium and reports which components rendered, why, and how to fix avoidable renders.
- What it does
- Opens a running React app in headless Chromium, replays scripted interactions, and records per-component render counts with causes (props, state, context, parent, recreated callbacks or values). Exposes MCP tools such as profile_url, run_scenarios, test_render_snapshots, compare_reports and inspect_component, and can compare runs against committed render snapshots or budgets. Reports include root-cause hints and suggested fixes such as React.memo, useCallback or useMemo.
- When to use it
- Useful when an assistant needs to measure whether a React change actually reduced re-renders, or when render counts should be checked in CI against a committed snapshot. Also for one-off investigation of why a component re-renders.
- Requirements
- Runs locally over stdio via the npm package crispy-profiling (npx crispy-profiling mcp). Needs Node.js, a Chromium build downloaded by the install step, a running development build of the React app reachable at a base URL, and a crispy.config.json describing scenarios. Optional login steps can read secrets from environment variables such as E2E_PASSWORD.
Installation
In SourceWeft
- Open Crispy Profiling in the dashboard and add it to a workspace.
- Enable the server for the chats that should use its tools.
Desktop only via STDIO. STDIO servers start a local process, so they need the SourceWeft desktop host.
Other MCP clients
Follow the launch instructions in the repository.
README
🥓 crispy-profiling
Snapshot testing for React re-renders — deterministic, runtime-proven, with the fix.
[CI] [npm] [License: MIT] [OpenSSF Scorecard]
[crispy test catches a PR that re-renders 20 rows, explains why and verifies the fix]
Status: early (0.x), improving every week. Validated on five open-source apps (Redux Essentials, Next.js App Router Playground, Excalidraw, shadcn-admin, react-admin): it found a fixable re-render problem in each. See Known limitations and the changelog. Bug reports, wrong hints and case studies are the most valuable contribution right now.
crispy-profiling opens your React app in headless Chromium, runs the interactions you describe, and tells you which components rendered, how many times, why (props / state / context / parent) and which renders were avoidable. Render counts are deterministic, so two reports of the same scenario only differ when the code changed. That makes it a reliable feedback loop for:
- AI coding agents: an MCP server and an Agent Skill so Claude Code, Cursor, Codex, Copilot & co. can measure a re-render fix instead of guessing.
- CI: render budgets and baseline comparison that fail a PR when a component starts re-rendering.
- You: a CLI that answers "why does this re-render?" without opening DevTools.
No code changes in your app: it uses the same hook React DevTools uses. Tested on React 19 and validated on 18.3 and 19.0 apps; React 16.8–17 expose the same hook but are not tested.
Quick start
From then on, npx crispy test (locally, in CI or from an AI agent) fails when a component starts
re-rendering, and tells you why and how to fix it:
For a one-off look at a flow, npx crispy run prints every component with its causes and a fix:
Every phase starts with Root causes — fix these first: the few components that recreate a
value, recreate a context value or update state that re-renders unchanged children, ranked by the
avoidable renders they cause, with the child where one React.memo would stop most of a cascade.
"Rendered at" and definedIn are mapped back to your original source files and lines through the
source maps your dev server or bundler serves (Vite, webpack, Turbopack); without source maps they
refer to the code the browser runs.
Works with Vite and Next.js (Turbopack and webpack dev servers); framework internals such as the Next.js dev overlay are filtered out. Profile the development build.
Render snapshots (crispy test)
Like Jest snapshots, but for re-renders. Commit the expected render counts of your key flows; every PR — written by a person or an AI agent — is checked against them at runtime.
When something regresses you get the component, the cause, where it is rendered and the fix (see Quick start).
crispy.snap.json has one line per component, so the PR diff shows exactly which counts changed:
Rules: any increase in commits, renders or avoidable renders fails (snapshot.tolerance allows
slack), and every metric is checked independently, so an improvement never hides a regression.
Counts that varied between runs are stored as [min, max] ranges and only fail outside them
(-u keeps the known range instead of narrowing it). Decreases pass and suggest -u. New UI
passes and is reported (record it with -u); if it already renders avoidably it is flagged ⚠️
(set snapshot.failOnNewAvoidable to fail instead). A known component that starts re-rendering
in a phase still fails. A rename, even combined with a move to another file, with the same counts
is reported as 🔁 renamed, not as a regression.
crispy test never edits the committed file on its own; the
snapshot always covers every component (even with topComponents); budgets still apply.
Configuration
crispy.config.json (JSON Schema):
Steps: click, hover, fill, type, press, select (a <select> option), drag
(selector to to, or by dx/dy, with pointer steps), scroll, waitFor (state:
visible, hidden, attached, detached), wait, goto, phase.
type presses one key at a time and waits for React to finish (including deferred values and
transitions) before the next key, so concurrent features give the same counts on fast and slow CPUs.
Renders before the first step are recorded in phase load; renders during steps go to
interaction unless you name phases yourself with { "action": "phase", "name": "..." }.
Budgets (per phase): maxCommits, maxTotalRenders, maxAvoidableRenders, maxWastedRenders,
and per component maxRenders / maxAvoidableRenders / maxWastedRenders.
A budget for a phase the scenario never produces is a config error; a component budget that never
matches a rendered component produces a warning (likely a typo). Budgets always see every component,
even when topComponents trims the report.
What the numbers mean
Every Markdown report (crispy run, crispy test, MCP tools) adds a Why / how to fix column
built from these fields. Hints point at the root cause: the component whose state starts a cascade,
the owner that recreates a prop (with its file:line), or the provider that recreates a context value.
Profile the development build: production builds minify component names.
CLI
Exit codes: 0 ok · 1 budget violation or regression · 2 usage/runtime error.
For AI agents
MCP server
Tools: profile_url, run_scenarios, test_render_snapshots, compare_reports, inspect_component.
Claude Code: claude mcp add crispy-profiling -- npx -y crispy-profiling@latest mcp
Claude Code plugin (MCP server + skill)
Agent Skill (Claude Code, Cursor, Codex, Copilot, Gemini CLI, …)
The skill teaches the agent the measure → fix → re-measure → compare loop and how to map each signal
to a fix (React.memo, useCallback, useMemo, context splitting, state colocation).
CI (GitHub Action)
The step fails when any component renders more than the committed snapshot allows (or a budget is
exceeded). The job summary — and one PR comment, updated on every push — lists each regression with
its cause, where it is rendered and the suggested fix (comment: false to disable). command: run (with an optional baseline report) is available for budget-only or
baseline-comparison setups. Full workflow: examples/github-workflow.yml.
Programmatic API
How it compares
Use the tools together: they answer different questions.
What crispy adds: deterministic counts (same code → same report), snapshots in CI, root-cause hints (who creates the unstable value, which dependency changes) and verification (the fix shows up as 🟢 improved).
Known limitations
- Development builds only. Production builds strip component names and the debug information crispy uses for causes and locations.
- Render counts are not milliseconds. crispy finds avoidable renders deterministically; whether
they matter depends on how expensive the components are. Use
timings: true(not reproducible) to see self time, LCP and long tasks. - Web only, Chromium only. No React Native; other browsers are not needed for render counts.
- Scenarios are written by hand (selectors and steps).
crispy initcreates a starting point. - Hints are heuristics. In our validation on real apps most hints pointed at the right component, but not all were directly actionable; report a wrong hint with the report attached and we will fix it.
- Apps with real network timing can vary between runs: counts are stored as ranges, randomness
is seeded and one extra commit is tolerated, but very timing-dependent flows may need
waitForsteps orclock: true.
How this is built
crispy-profiling is developed with AI coding agents (Claude Code) under human direction, with the same rules as any contribution: atomic commits, tests, CI on Node 20/22/24 and review. Every milestone is checked by an independent agent acting as a hostile reviewer and validated on real open-source apps; every number in this README and in the changelog can be reproduced with the commands shown. Found something wrong? Please open an issue.
How it works
An init script installs (or wraps) __REACT_DEVTOOLS_GLOBAL_HOOK__ before React loads. On every
commit it walks the new fiber tree against its alternate, like React DevTools, and records each
component that performed work, comparing props, hook state and context values to classify the cause.
See docs/ARCHITECTURE.md.
Contributing
Issues and PRs are welcome — see CONTRIBUTING.md (GitFlow: branch from
develop). AI coding agents: start with AGENTS.md; docs index for LLMs:
llms.txt.
License
MIT © Edwin Jorge
Source: README.md at commit dc27f6a
Tools
0Version history
1- v0.1.0LatestOct 5, 2026


