0
components rendered
a React app, as crispy sees it: 227 components
rendered with nothing new to show
rendered for a reason · idle

One keystroke.
One line.
One fix.
Kept fixed.

You typed one letter. React ran 1,227 components. 1,206 of them had nothing new to show. You felt it as lag. Nobody saw it.

crispy saw it. It opens your app in headless Chromium, counts every render, and names the line: src/App.tsx:222.

Wrap the callback. Memoize the row. Same keystroke, 21 renders. The report is byte-for-byte reproducible, so the number is a fact, not a feeling.

npx crispy test snapshots the counts. The pull request that brings the 1,206 back fails, with the cause and the fix in the comment.

src/App.tsx:222 (App)Row · 20 renders · 20 avoidable
onSelect={(id) => setSelected(id)}
onSelect={useCallback((id) => setSelected(id), [])}
🟢 crispy test · pass
↓ scroll to see what happened
↓ keep scrolling · the four steps move sideways
01 — 04

Find it.

npx crispy scan detects Vite or Next.js, starts your dev server, visits a few routes and tries the safe interactions: buttons, tabs, inputs, links. Never anything named delete, pay, sign out or submit, in several languages. Read-only: writes never leave the browser.

02 — 04

Explain it.

Every render gets a reason: props, state, context or parent. The avoidable ones get a root cause: a callback recreated on every render, an inline object with equal data, a context value rebuilt each time. Plain English first, the technical detail after.

03 — 04

Fix it.

Each finding points at a file and a line and says what to do: useCallback, useMemo, hoist, React.memo. Your coding agent can apply it through the MCP server and measure that it worked, instead of guessing.

04 — 04

Keep it fixed.

npx crispy test records crispy.snap.json. Commit it. In CI the GitHub Action fails the pull request that adds renders and posts one comment with the cause and the fix. The snapshot is the gate; milliseconds are only information.

Try it here

Type. Click. Read the bill.

A small list app, instrumented the way crispy sees it. Every keystroke updates App state; the report on the right is what crispy would print.

2Click a row. A dark flash means it rendered for a real change.
3Switch the code to with the fix and type again. Read the verdict at the bottom of the report.
scenario · search · 3 runs
code:
App0
Header0
Status08 members · none selected
avoidable   real change   · click a row to select it

      
ComponentRendersAvoidableWhy it rendered · how to fix
Type in the box to profile the scenario.

One tool. Four doors.

Pick the one you will use first. Each opens with the commands to copy. Start with the terminal: three lines to the first finding.

01

Terminal

+

Zero config to the first finding. Then a snapshot you commit, and a test that reads it.

  • scan finds and profiles interactions, writes crispy.config.json.
  • run prints every component with its causes and the fix.
  • test records or checks crispy.snap.json; --ci fails on regressions.
  • compare diffs two reports. Exit codes: 0 ok · 1 regression · 2 usage.
npm i -D crispy-profiling
npx crispy install        # Chromium once; or CRISPY_CHROMIUM_PATH
npx crispy scan           # find · explain · write the config
npx crispy test           # record crispy.snap.json, commit it
npx crispy test --ci      # fail when a component adds renders
02

GitHub Action

+

Fails the pull request that adds renders. Installs Chromium itself. One PR comment, updated on every push, with the cause, the place and the fix.

  • Pin @v0 or a release tag.
  • scenarios input splits a slow suite across a job matrix.
  • About 20–25 s per scenario on a GitHub runner.
name: crispy
on: pull_request
permissions: { contents: read, pull-requests: write }
jobs:
  renders:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: npm ci
      - uses: edgeorgie/crispy-profiling@v0
03

MCP server

+

For the agent that writes the fix. Claude Code, Cursor, Codex, Copilot & co. profile before and after, and get the same numbers for the same code.

  • Tools: scan_app, profile_url, test_render_snapshots, compare_reports, inspect_component.
  • Listed in the MCP Registry as io.github.edgeorgie/crispy-profiling.
// .mcp.json
{
  "mcpServers": {
    "crispy-profiling": {
      "command": "npx",
      "args": ["-y", "crispy-profiling@latest", "mcp"]
    }
  }
}
# Claude Code
claude mcp add crispy-profiling -- npx -y crispy-profiling@latest mcp
04

Skill & plugin

+

The know-how, packaged. The react-render-profiling Agent Skill teaches when to profile, how to read a report and how to apply each hint. The Claude Code plugin bundles it with the MCP server.

# inside Claude Code
/plugin marketplace add edgeorgie/crispy-profiling
/plugin install crispy-profiling@crispy-profiling

# then just ask
"Why does the list re-render when I type? Profile it, fix it, prove it."
0lines of config before the first finding
0changes to your app: it uses the hook React DevTools uses
5/5open-source apps profiled, a fixable re-render problem found in each
2.5′to the first root causes on shadcn-admin, without a line of config

What it prints.

No dashboards. A short receipt: the root causes first, each with the file, the line and the fix. Everything else is in the JSON for when you want it. Reports are deterministic: sorted, no timestamps, so two runs of the same code are identical.

$ npx crispy scan
click the command to copy the three install lines