关于 Crispy Profiling
crispy-profiling 通过命令行、GitHub Action、MCP 服务和 Agent Skill 分析 React 渲染。它报告组件渲染次数、可避免的渲染及其原因和修复建议,并可比较快照以发现回归。
主要功能
- 01在 Chromium 中分析组件渲染
- 02识别可避免的渲染及原因
- 03提供源代码级修复建议
- 04快照记录渲染次数以检查回归
- 05在 CI 和 Playwright 测试中运行
- 06提供 MCP 服务和 Agent Skill
适用场景
- 01查找 React 交互卡顿的原因
- 02检查拉取请求中的渲染变化
- 03衡量 AI Agent 的 React 优化效果
- 04在 Playwright 流程中加入渲染检查
MCP 相关
其 MCP 服务使 AI Agent 能检查 React 渲染分析结果,并在评估代码变更时参考原因和修复建议。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
6收录的工具列表 · 更新于 3 周前
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| top | integer | 否 | 每个阶段显示的组件数 |
| url | string | 是 | 要分析的页面,例如 http://localhost:5173/products |
| runs | integer | 否 | |
| steps | array | 否 | 加载后要执行的交互。交互期间的渲染会归入阶段 "interaction"。 |
| outFile | string | 否 | 写入完整 JSON 报告的可选路径 |
| timings | boolean | 否 | 同时测量每个阶段的 JavaScript 毫秒数(与渲染次数不同,每次运行的数值会变化):用于告知用户交互速度提升了多少。 |
详细介绍
🥓 crispy-profiling
React 重渲染的快照测试——结果确定、经运行时验证,并附带修复方案。
crispy-profiling 是一款 React 重渲染分析工具(npm:crispy-profiling:CLI、GitHub Action、MCP 服务和 Agent Skill。它与 crispy 风格指南包或同名科学软件无关。
输入或点击时,你的 React 应用感觉很慢?crispy 会指出哪些组件在做无意义的重渲染,以及要修复的确切代码行。
快速开始
npm i -D crispy-profiling # or: pnpm add -D crispy-profiling · yarn add -D crispy-profiling
npx crispy install # downloads Chromium once (if that fails, an installed Chrome/Chromium is used)
npx crispy scan # zero config: starts your dev server, finds interactions, profiles them
npx crispy test # records crispy.snap.json from the scanned scenarios → commit it
使用 pnpm 或 yarn?运行相同命令,分别写作 pnpm exec crispy … 或 yarn crispy …。
刚开始使用?先了解这三点。
crispy scan只会检查你的应用:不会编辑任何代码,也不会向任何地方发送数据(会阻止可能更改数据的请求)。- 它会打印一份简短的根本原因列表。每项都会指出文件和代码行,并说明要修改什么。“没有可避免的渲染”表示它尝试过的流程没有问题;
Not tried行会说明它跳过了什么,而crispy.config.json是你添加感觉缓慢的交互流程的地方。 - 修复后,再次运行
npx crispy test:🟢 表示渲染次数减少了(请确认屏幕仍会更新),🔴 表示又出现了渲染。除非你主动指定(-u),否则不会覆盖保存的快照。
你会看到这些术语:render 指 React 再次运行某个组件;avoidable 表示输入实际上没有变化,因此即使没有这次渲染,屏幕看起来也会一样;hoist 指将常量移到组件外;memoize 指在多次渲染之间保留相同的值或函数(useMemo、useCallback、React.memo)。除非使用 React.memo 包装,否则每当父组件渲染时,组件都会重新渲染,即使 props 没变;而 React.memo 只有在 props 保持稳定时才会跳过渲染,因此 crispy 通常会同时给出这两方面的建议。
crispy scan 会检测 Next.js/Vite、开发 URL(即开发服务打印的端口,例如 vite.config.ts 中的 server.port)和开发命令;它会访问几个路由,尝试其中安全的交互操作(按钮,包括仅含图标的按钮、选项卡、选择框、文本输入框、内部链接、鼠标指针样式为手形的表格或列表行——不会触发名称中包含 delete、pay、sign out、submit… 等字样的操作,且会识别多种语言),并打印最主要的根本原因及修复方案。与所有 crispy 命令一样,它是只读的:GET 以外的请求和页面通过 WebSocket 发送的消息都不会离开浏览器(热重载套接字除外);如果某项交互尝试发送请求,系统会报告该情况,且不会保存该交互(对于数据可随时丢弃的应用,可使用 --allow-writes)。请将它指向开发版或预览版,而不是生产环境。它会将已运行的流程保存为 crispy.config.json,此后 crispy test 就会保护这些流程。它会将 .crispy/(报告)添加到 .gitignore;请提交 crispy.config.json 和 crispy.snap.json。在 shadcn-admin 上,无需编写任何配置,它就在 2.5 分钟内找到了首批根本原因。更喜欢自己编写步骤?运行 npx crispy init,即可创建配置并进行编辑。
从那以后,无论是在本地、CI 中还是通过 AI Agent 运行 npx crispy test,当组件开始重新渲染时,测试都会失败,并告诉你原因以及如何修复:
| 🔴 regressed | list / interaction | Row | renders | — → 20 | recreated on every render (rendered at
src/App.tsx:222 (App)): `onSelect` is a new function with the same code in `App`: wrap it in
useCallback with the values it uses as dependencies. Then wrap this component in React.memo. |
要临时查看某个流程,可运行 npx crispy run,它会打印每个组件、对应的原因和修复方案:
| Component | Renders | Avoidable | Callback | Why it rendered | Rendered at | Why / how to fix |
| Row | 20 | 0 | 20 | recreated callbacks 20 | `src/App.tsx:222 (App)` | `onSelect` is a new function with the same code in `App`: wrap it in useCallback… |
| Status | 1 | 1 | 0 | recreated props 1 | `src/App.tsx:178 (App)` | `style` is recreated with equal data in `App`: hoist it out of the component or wrap it in useMemo… |
| App | 1 | 0 | 0 | own state 1 | `src/main.tsx:12` | state updates here cause 23 avoidable render(s) below (`Row`, `Header`, `Status`)… |
每个阶段都会从根因——优先修复这些问题开始:这些少数几个组件会重新创建某个值、重新创建某个上下文值,或更新状态,导致未改变的子组件重新渲染。它们会按所导致的可避免渲染次数排序,并指出只需添加一个 React.memo 就能阻止大部分级联渲染的子组件。
“Rendered at”和 definedIn 会通过开发服务器或打包工具提供的源码映射(Vite、webpack、Turbopack),映射回原始源文件及行号;没有源码映射时,它们指的是浏览器运行的代码。
兼容 Vite 和 Next.js(Turbopack 和 webpack 开发服务器);Next.js 开发覆盖层等框架内部内容会被过滤。请对开发构建进行性能分析。

状态:早期版本(0.x),每周都在改进。 已在五个开源应用(Redux Essentials、Next.js App Router Playground、Excalidraw、shadcn-admin、react-admin)上验证:每个应用都发现了可修复的重新渲染问题。请参阅已知限制和更新日志。目前最有价值的贡献是提交 Bug 报告、指出错误提示和分享案例研究。
crispy-profiling 会在无头 Chromium 中打开你的 React 应用,运行你描述的交互,并告诉你哪些组件进行了渲染、渲染了多少次、原因是什么(props/状态/上下文/父组件),以及哪些渲染本可避免。每个组件的渲染次数都可复现,因此同一场景的两份报告只有在代码变更时才会有所不同(提交次数和副作用级联可能随负载时序变化;快照会将它们存为范围)。因此,它能为以下场景提供可靠的反馈循环:
- AI 编码 Agent:提供 MCP 服务和 Agent Skill,让 Claude Code、Cursor、Codex、Copilot 等可以测量重新渲染修复效果,而不是靠猜测。
- CI:设置渲染预算并与基线比较;当组件开始重新渲染时,让 PR 检查失败。
- 你:使用命令行回答“为什么会重新渲染?”,无需打开 DevTools。
无需更改应用代码:它使用的正是 React DevTools 使用的同一个 hook。已在 React 19 上测试,并在 React 18.3 和 19.0 应用上验证;React 16.8–17 也提供同一个 hook,但尚未测试。
渲染快照(crispy test)
类似 Jest 快照,但用于重新渲染。提交关键流程的预期渲染次数;每个 PR——无论由人还是 AI Agent 编写——都会在运行时与其比较。
npx crispy test # 1st run: writes crispy.snap.json → commit it
npx crispy test # later: fails if any component renders more (or more avoidably)
npx crispy test -u # accept intended changes / lock in improvements
npx crispy test --ci # in CI: a missing snapshot fails instead of being written (auto-detected; --no-ci to opt out)
发生回归时,你会看到组件、原因、渲染位置和修复方法(请参阅快速入门)。
crispy.snap.json 每个组件占一行,因此 PR 差异会准确显示哪些次数发生了变化:
"interaction": {
"commits": 1,
"components": {
"App": { "renders": 1, "avoidable": 0 },
"Header": { "renders": 1, "avoidable": 1 }
}
}
规则:渲染次数或可避免渲染次数只要增加就会导致检查失败(snapshot.tolerance 可允许一定浮动);每项指标都会单独检查,因此性能改善不会掩盖回归。提交次数增加会显示为 ℹ️,但不会导致检查失败,因为它会随负载时序变化(设置 snapshot.failOnMoreCommits 可让其导致检查失败)。运行间存在波动的次数会存为 [min, max] 范围,只有超出范围才会导致检查失败(-u 会保留已知范围,而不缩小范围)。次数减少时检查会通过,并建议使用 -u。新增 UI 会通过检查并在报告中显示(使用 -u 记录);如果它已经存在可避免的渲染,则会标记为 ⚠️(也可设置 snapshot.failOnNewAvoidable,让其导致检查失败)。当渲染次数不变,但其中可避免的次数增加时——这通常发生在修复揭示出下一个原因后——该行会显示 🟡“现在可通过修复避免”,且不会导致检查失败(设置 snapshot.failOnMoreAvoidable 可让其导致检查失败)。已知组件在某个阶段开始重新渲染时,检查仍会失败。重命名(即使同时移动到其他文件)只要次数相同,就会报告为 🔁“已重命名”,而非回归。
crispy test 绝不会自行编辑已提交的文件;快照始终涵盖每个组件(即使设置了 topComponents);预算仍然适用。
在 Playwright 测试中
已经有 Playwright e2e 测试?直接在测试中检查重新渲染,无需单独编写场景:
import { test } from '@playwright/test';
import { renders } from 'crispy-profiling/playwright';
test('search does not re-render the product list', async ({ page }) => {
const r = await renders(page); // before page.goto
await page.goto('/');
await r.phase('search');
await page.fill('#search', 'shoes');
await r.toMatchSnapshot('search'); // __renders__/search.snap.json; fails with cause + fix
});
首次运行会写入快照(请提交);CRISPY_UPDATE=1 会接受预期的更改;在 CI 上(设置了 CI)如果快照缺失,测试会失败。r.report() 会返回完整报告,供自定义断言使用。
配置
crispy.config.json(JSON Schema):
{
"$schema": "./node_modules/crispy-profiling/schema/crispy.config.schema.json",
"baseUrl": "http://localhost:5173",
"runs": 3,
"scenarios": [
{
"name": "search",
"path": "/products",
"steps": [
{ "action": "type", "selector": "#search", "value": "shoes" },
{ "action": "phase", "name": "sort" },
{ "action": "click", "selector": "text=Price: low to high" }
],
"budgets": {
"interaction": { "maxAvoidableRenders": 0, "components": { "ProductCard": { "maxRenders": 20 } } }
}
}
]
}
| 字段 | 默认值 | 描述 |
|---|---|---|
baseUrl | — | 正在运行的应用的来源。 |
runs | 3 | 每个场景的运行次数;报告会保留中位数、最小值、最大值,并标记不稳定的计数。 |
settleMs | 300 | 如果在这段时间内没有 React 提交,并且没有进行中的网络请求,则认为一个步骤已“稳定”。 |
maxSettleMs | 10000 | 每个步骤的最长等待时间。对于始终无法稳定的页面(轮询、动画),报告会发出警告,而不会一直等待。 |
cpuThrottle | 1 | 降低 CPU 速度(例如 4),以便在速度较慢的 CI 运行器或低端设备上检查计数。计数不应发生变化。 |
clock | false | 使用虚拟时钟控制计时器(setTimeout、setInterval、requestAnimationFrame、Date、performance),让轮询或带动画的应用产生确定性计数。 |
timeoutMs | 30000 | 导航、步骤或稳定等待的最长时间。 |
readOnly | true | 性能分析期间,阻止除 GET 以外的所有请求,以及页面发送的 WebSocket 消息(仅豁免开发服务器自身的热重载套接字),以确保重放点击不会更改数据。WebSocket 连接本身仍会建立,也可以接收消息:只会丢弃页面发送的内容。被阻止的写入操作会列在报告的警告中。只有必须执行写入的流程(例如连接到一次性测试数据库)才设为 false。 |
timings | false | 添加每个阶段的主线程 CPU 数据(cost:JavaScript 和所有主线程工作的耗时,单位为毫秒,生产构建中也适用)、组件自身耗时以及 LCP/CLS/长任务数据。默认关闭,因为计时结果无法复现。 |
topComponents | 0 | 每个阶段仅保留渲染次数最多的 N 个组件(0 = 全部)。 |
viewport | 1280×800 | 浏览器视口。 |
browser | headless | executablePath、channel(例如 "chrome")、headless。也支持 CRISPY_CHROMIUM_PATH。 |
includeInternals | false | 显示框架/库的内部组件(由 node_modules 中定义、且仅由库代码渲染的组件,例如 Next.js 路由器内部组件)。由你的代码直接渲染的库组件始终会显示。 |
webServer | — | { "command": "npm run dev" }:crispy 会启动开发服务器,等待 baseUrl(或 url)可用,然后停止服务器;按 Ctrl-C 时也会停止。在本地,如果该地址已有服务器运行,则会复用(并发出警告);CI 上则不会复用,而是直接失败(可通过 reuseExisting 覆盖)。如果 baseUrl 始终无响应,但服务器公布了另一个本地 URL,crispy 会使用该 URL 并告知你。crispy init 会填入此配置。 |
login | — | { "path": "/login", "steps": [...] }:性能分析前登录一次(不会计入统计)。使用 "${E2E_PASSWORD}" 从环境中读取密钥("$${NAME}" 会输入字面值 ${NAME})。 |
storageState | — | 已保存的会话文件(Cookie + localStorage),例如用于 SSO/OAuth 登录的 crispy login 输出文件。不要将它提交到 git。 |
random | seeded | Math.random 在每次运行中都会返回相同的序列,因此模拟数据、ID 和动画的渲染方式都相同。native 则保留浏览器原本的行为。 |
snapshot | crispy.snap.json、0、false、false、false、false | crispy test 使用的 file(相对于配置文件的路径)、tolerance、failOnNewAvoidable、failOnMoreAvoidable、failOnMoreCommits 和 includeLibraries(也记录 node_modules 中定义的组件,例如图标;默认关闭,因为它们的计数会随渲染它们的应用组件变化)。 |
compare | 10%、1 | compare 使用的 rendersIncreasePct 和 minRendersDelta。 |
步骤:click、hover、fill、type、press、select(一个 <select> 选项)、drag | ||
(通过 selector 拖至 to,或通过 dx/dy 拖动,并设置指针的 steps)、scroll、waitFor(state 可选: | ||
visible、hidden、attached、detached)、wait、goto、phase 和 expect(对 selector 检查 | ||
text 或 count):如果屏幕显示的内容不符合预期,就会导致场景失败,因此冻结 UI 的修复会使 | ||
crispy test 失败,而不是因为渲染次数减少而通过。在每个结果重要的交互之后添加一个步骤,例如 { "action": "expect", "selector": "#cart-count", "text": "3" }。 | ||
text 和可见性会针对可见匹配项进行检查,因此隐藏的重复项(例如移动端菜单、屏幕阅读器标签) | ||
不会导致步骤失败;count 会统计所有匹配项(使用 "selector >> visible=true" 仅统计可见项)。如果场景中的 | ||
| 某个步骤失败,该场景会被跳过并在输出中列出名称:其他场景仍会生成报告或进行快照比较。 | ||
type 会逐个按键输入,并在输入下一个按键前等待 React 完成更新(包括延迟值和过渡),因此并发功能在 | ||
快速和慢速 CPU 上都会产生相同的计数。第一个步骤之前的渲染会记录在 load 阶段;步骤期间的渲染会归入 | ||
interaction 阶段,除非你使用 { "action": "phase", "name": "..." } 自行命名阶段。 |
预算(每个阶段):maxCommits、maxTotalRenders、maxAvoidableRenders、maxWastedRenders,以及每个组件的 maxRenders / maxAvoidableRenders / maxWastedRenders。
如果场景未产生某个阶段,却为该阶段设置了预算,就会出现配置错误;如果组件预算未匹配到任何已渲染组件,
则会发出警告(可能是拼写错误)。无论 topComponents 是否缩减报告内容,预算始终会纳入所有组件。
数字的含义
| 字段 | 含义 |
|---|---|
renders | 组件函数/类渲染的次数(挂载 + 更新)。 |
avoidableRenders | 实际没有任何变化的更新:浪费的渲染,以及仅由重新创建但内容相同的数据(对象、数组、元素、日期、映射……)导致的渲染。肯定可以避免。 |
callbackRenders | 仅由重新创建的同代码函数(内联回调)导致的更新。如果这些函数捕获的值没有变化,就可以避免——crispy 无法查看捕获的值,因此会单独报告。 |
wastedRenders | props(浅比较)、状态和使用的 context 均未变化的更新。 |
causes.props / state / context | 对应输入发生变化的更新(一次更新可能有多个原因)。 |
causes.unstable | 仅数据标识发生变化:对象/数组字面量、日期、映射、context 值或内容相同但重新创建的 hook 结果。 |
causes.callback | 仅函数被重新创建,但代码相同(绑定函数/原生函数算作实际变化)。 |
causes.parent | 没有输入发生变化的更新:父组件重新渲染(与 wasted 相同)。 |
unstableProps | 以相同数据重新创建的 prop 键——使用 useMemo 修复,或将常量提到组件外。 |
callbackProps | 作为回调重新创建的 prop 键——使用正确的依赖项配合 useCallback 修复,或使用 React Compiler。 |
changedProps | 标识发生变化的 prop 键及其次数——说明 causes.props 背后的原因。 |
triggeredBy | 自身状态更新启动了级联、从而导致此组件重新渲染的组件及其次数。修复触发源,而不是每个子组件。 |
recreatedContextFrom | 拥有 context provider 的组件;该 provider 的 value 被重新创建,但内容相同(例如 value={{ user, logout }})——在该处记忆化此值。 |
memo | 当组件使用 React.memo 包装时为 true,因此提示不会建议再次包装它。 |
memoSkips / uselessMemo | 在此场景(所有阶段)中 React.memo 跳过的渲染次数。更新至少 3 次却从未跳过一次的 memo 组件会被标记为 uselessMemo,并提示考虑移除它——这是防止所有组件都使用 memo 的保护措施。 |
stateChanges | 组件自身状态导致渲染时,发生变化的状态:如果 hook 写在组件中,则为 `query` (useState);如果是 store 和路由器 hook(Redux、Zustand、路由器),则为 store subscription (useSyncExternalStore) in useAppSelector``;如果多个自定义 hook 都可能持有该状态,则会显示 "in one of …"。React DevTools 只显示 hook 编号。 |
effectCascades | 组件中的 useEffect 在渲染后立即设置的状态:组件自身的状态(`d` (useState))、通过 setter prop 设置的父组件状态(… in `Parent` (via a prop)),或 store 状态(a store read by `Reader`)。每项都会产生一次额外提交(cascadeCommits,包含 cascadeRenders 次渲染);提示会说明如何移除它(在渲染期间计算、在事件处理程序中设置,或在那里调用父组件的 setter)。不计入:布局 effect(测量 DOM)、过渡、延迟值,以及因组件挂载而运行的 effect。React 18+ 需要使用 createRoot。 |
creators | `prop |
staleMemo | `prop |
providerAt | 重新创建的 context 值的 provider 渲染位置。 |
compiled | React Compiler 编译该组件时为 true。 |
locations | 该组件渲染位置中最多 3 个,格式为 file:line (Owner)(owner JSX 调用位置,按出现频率从高到低),如果有 source map 则通过它解析。 |
Item (src/List.tsx) keys | 组件按名称和定义它们的文件识别(通过 DevTools 协议解析)。同名的不同组件会以 Name (file) 为键,因此添加无关的 Item 不会重命名已有组件;快照会存储文件并据此匹配。如果无法通过文件区分(styled-components、HOC 工厂、单个文件中的多个组件),则按其渲染位置作为键(Item @ src/Card.tsx:12);带编号的键(Item#2)是最后的备选方案。在 goto 导航过程中,键保持不变。 |
definedIn | 定义组件函数的文件。 |
stable | 如果运行之间的计数不同(计时器、网络、随机性),则为 false。 |
每份 Markdown 报告(crispy run、crispy test、MCP 工具)都会新增一列 原因 / 修复方法, | |
内容根据这些字段生成。提示会指出根本原因:状态变化引发一连串更新的组件、重新创建 prop 的所有者(附带其 file:line),或重新创建 context 值的 provider。 |
请分析开发构建:生产构建会缩短组件名称。
CLI
crispy scan [url] [--routes 3] [--actions 5] [--allow-writes] Zero config: find, profile and save interactions
crispy init [--base-url <url>] Create crispy.config.json (detects framework, URL, dev command)
crispy install [--with-deps] [--verbose] Download the Chromium build crispy uses (--verbose: full download log)
crispy login [-c file] [--path /login] Sign in by hand in a browser window and save the session
crispy run [-c file] [-o file] [-s scenario...] [--markdown file] [--no-fail]
crispy test [-c file] [-u|--update] [--ci] [-s scenario...] [--markdown file]
crispy compare <base.json> <head.json> [--threshold 10] [--min-delta 1] [--markdown file] [--json file] [--no-fail]
crispy mcp Start the MCP server on stdio
退出代码:0 正常 · 1 超出预算、出现回归或 expect 步骤失败 · 2 用法 / 运行时错误。
面向 AI Agent
MCP 服务
工具:scan_app(尚未配置时:查找交互并返回最主要的原因)、profile_url、
run_scenarios、test_render_snapshots、compare_reports、inspect_component。
{
"mcpServers": {
"crispy-profiling": { "command": "npx", "args": ["-y", "crispy-profiling@latest", "mcp"] }
}
}
Claude Code:claude mcp add crispy-profiling -- npx -y crispy-profiling@latest mcp
Claude Code 插件(MCP 服务 + 技能)
/plugin marketplace add edgeorgie/crispy-profiling
/plugin install crispy-profiling@crispy-profiling
Agent 技能(Claude Code、Cursor、Codex、Copilot、Gemini CLI 等)
npx skills add edgeorgie/crispy-profiling
该技能会教 Agent 如何执行“测量 → 修复 → 重新测量 → 对比”循环,以及如何根据每种信号选择修复方法
(React.memo、useCallback、useMemo、拆分 context、状态就近放置)。
CI(GitHub Action)
保存为 .github/workflows/crispy.yml:
name: crispy
on: pull_request
permissions:
contents: read
pull-requests: write # lets crispy comment on the PR
jobs:
renders:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with: { node-version: 22 }
- run: npm ci
- uses: edgeorgie/crispy-profiling@v0 # starts your dev server (webServer) and runs `crispy test --ci`
该 Action 会自行安装 Chromium 及其系统库:工作流无需设置浏览器。
首次运行前,请记录并提交基线:在本地运行 npx crispy test,然后提交
crispy.snap.json。如果没有基线,任务会因“missing snapshot”失败(这是有意设计的:CI 不会替你记录基线)。@v0 是适用于 0.x 版本的一种浮动标签;如需固定版本,请指定 @v0.2.0(或提交 SHA)。对于来自 fork 的拉取请求,GitHub 不会授予令牌写入权限,因此会跳过 PR 评论并显示警告;任务摘要和通过 / 失败结果仍会正常显示。
如果任务显示红色:打开任务摘要,修复其中指出的原因,然后再次推送。如果更改是有意的,则由人工运行 npx crispy test -u 并提交新快照。
每个场景会运行 3 次(runs),因此包含许多场景的配置可能需要几分钟。使用矩阵和 scenarios 输入将任务拆分到多个运行器(名称之间用空格或逗号分隔);每个任务只检查已提交快照中对应的部分:
strategy:
matrix:
scenarios: ['tasks users', 'settings sidebar-toggle']
steps:
# checkout, setup-node, npm ci as above
- uses: edgeorgie/crispy-profiling@v0
with: { scenarios: '${{ matrix.scenarios }}' }
在本地进行相同拆分时,运行 npx crispy test -s tasks -s users。预算:在 GitHub 运行器上,每个场景每次运行约需 20–25 秒,另加一分钟安装 Chromium。
只要有任何组件的渲染次数超过已提交快照允许的数量(或超出预算),该步骤就会失败。任务摘要以及每次推送时更新的一条 PR 评论,会列出每项回归及其原因、渲染位置和建议修复方法(设置 comment: false 可禁用评论)。command: run(可选附带 baseline 报告)适用于仅检查预算或与基线对比的设置。完整工作流:examples/github-workflow.yml。
编程 API
import { compareReports, parseConfig, profile } from 'crispy-profiling';
const config = parseConfig({ baseUrl: 'http://localhost:5173', scenarios: [{ name: 'home' }] });
const report = await profile(config);
对比
结合使用这些工具:它们回答的问题各不相同。
| 工具 | 用途 | crispy 的定位 |
|---|---|---|
| React DevTools Profiler | 在浏览器中交互式手动分析性能 | crispy 以无头模式对脚本化交互执行同类分析,每次都如此 |
| React Scan | 使用应用时以可视化方式突出显示重新渲染;也提供程序化的 onRender API | crispy 将渲染次数转化为已提交的快照,并让 CI 检查失败,同时为每个组件提供修复提示 |
| why-did-you-render | 在开发环境中通过控制台通知提示可避免的重新渲染(需要 Babel 配置) | crispy 无需修改应用,并且会以确定性方式按交互报告 |
| React Doctor | 对代码库进行静态分析(lint 规则)并给出评分 | crispy 观察运行时实际渲染的内容;静态发现与运行时证据可以互为补充 |
| React Compiler | 在构建时自动进行记忆化 | crispy 会显示编译器未覆盖的内容(例如每次渲染都会变化的依赖项),并验证结果 |
crispy 的新增价值:确定性的计数(相同代码 → 相同报告)、CI 中的快照、 根因提示(谁创建了不稳定的值、哪个依赖项发生变化),以及验证 (修复会显示为 🟢 已改善)。
已知限制
- 🟢 表示渲染次数减少,不代表 UI 正常工作。 对读取可变数据(如表格或表单实例)的组件使用 React.memo,会减少渲染次数,也会阻止更新。修复后,请检查受影响的屏幕是否仍会在应当变化时更新。
- 仅支持开发构建。 生产构建会移除组件名称和 crispy 用于确定原因及位置的调试信息。
- 渲染次数不等于毫秒数。 crispy 会以确定性方式找出可避免的渲染;这些渲染是否重要,取决于组件的开销。使用
timings: true(结果不可复现)可查看每个阶段消耗的 CPU 时间(毫秒),以及自身耗时、LCP 和长任务。 - 测量本身有开销。 渲染钩子会增加主线程工作量(在 shadcn-admin 上,JavaScript 耗时约增加 25 %):请比较多次 crispy 运行之间的毫秒数,而不要与未插桩的浏览器进行比较。
- 仅限 Web,仅限 Chromium。 不支持 React Native;对于渲染次数分析,不需要其他浏览器。
- 场景由手动编写,或通过
crispy scan生成(仅包含安全交互)。 - 提示基于启发式判断。 在我们对真实应用的验证中,大多数提示都指向了正确的组件,但并非所有提示都能直接采取行动;如果发现错误提示,请附上报告并报告错误提示,我们会进行修复。
- 具有真实网络时序的应用在不同运行之间可能会有差异:计数以范围形式存储,随机性经过设定种子,并且容许多出一次提交,但对时序依赖很强的流程可能需要
waitFor步骤或clock: true。
构建方式
crispy-profiling 在人工指导下使用 AI 编码 Agent(Claude Code)开发,并遵循与任何贡献相同的规则:原子提交、测试、在 Node 20/22/24 上运行 CI,以及代码审查。每个里程碑都会由扮演严格审查者的独立 Agent 进行检查,并在真实开源应用上验证;本 README 和变更日志中的每个数字,都可以通过所示命令复现。发现问题?请提交 issue。
工作原理
初始化脚本会在 React 加载前安装(或包装)__REACT_DEVTOOLS_GLOBAL_HOOK__。每次提交时,它都会像 React DevTools 一样遍历新的 fiber 树并与其 alternate 对比,记录每个执行了工作的组件,并比较 props、hook 状态和 context 值,以判断原因。
请参阅 docs/ARCHITECTURE.md。
参与贡献
在你的应用上试过了吗?只需五行:应用及版本、命令、crispy 输出的内容、你的预期、你做了哪些更改,以及计数发生了什么变化。提交一个
“我在自己的应用上试过了”
问题;错误提示也有专用表单。
也欢迎提交 PR — 请参阅 CONTRIBUTING.md(GitFlow:从 develop 创建分支)。AI 编码 Agent:从 AGENTS.md 开始;面向大模型的文档索引:
llms.txt。
许可证
MIT © Edwin Jorge
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。