如何用 react-scan/lite 无头插桩获取 commit 与 render 事件流
【免费下载链接】react-scanScan and fix React performance issues项目地址: https://gitcode.com/GitHub_Trending/re/react-scan
如果你需要在不引入 react-scan 工具栏、不渲染任何 UI 的前提下,以编程方式拿到 React 应用的 commit 与 render 事件流——比如把每个组件的重渲染原因记录到日志,或把事件 POST 到一个 ingest 端点——react-scan/lite就是这个用途。它是一个极小的无 UI 入口,接入的是 React DevTools 的 profiling 通道(Timeline Profiler 使用的同一通道),产出的是 commit / render / state-update 等事件流,可以通过回调消费,也可以通过fetch上报到端点。
准备条件:安装与版本要求
安装 react-scan 主包,react-scan/lite是它的子路径导出,无需单独安装:
npm i react-scan或使用 pnpm / yarn:
pnpm add react-scanyarn add react-scan包的 peer 依赖要求 React / react-dom 在^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0范围内。package.json的exports中声明了./lite子路径,分别映射到dist/lite/index.mjs与dist/lite/index.js。
插入时机:必须在 react-dom 挂载之前
instrument()必须运行在react-dom挂载之前:放在入口文件最顶部,或作为<head>中的第一个 script。原因与 react-scan 主入口一致——它需要在 React 拿到__REACT_DEVTOOLS_GLOBAL_HOOK__之前接管这个 hook(参考 docs/installation/vite.md 中的说明)。
instrument()是 SSR 安全的:在 Node 环境中调用会返回一个 noop handle,其isActive()返回false,所有方法可安全调用而不抛错。
最小插入示例
下面的主路径只接回调,把事件打印到控制台(示例来自 packages/scan/README.md 的react-scan/lite章节):
import { instrument } from "react-scan/lite"; const handle = instrument({ onEvent: (event) => console.log(event), }); // 结束时调用,幂等 handle.stop();instrument()返回LiteHandle,包含三个成员:
subscribe(listener):追加一个监听器,接收所有已发出的事件,返回一个取消订阅函数。onEvent与subscribe可以组合使用,回调先于订阅者触发;isActive():当前是否仍在发出事件;stop():停止发事件,幂等。调用后 hook 上被替换的 handler 会被还原为原始实现。
事件对象LiteEvent的公共字段是kind和timestamp,按需附带componentName、lanes、laneLabels、priorityLevel、priorityName、tree、didError等字段,完整定义见 packages/scan/src/lite/types.ts。
理解事件流
LiteEventKind覆盖以下类别(节选自 types.ts):
- 通道状态:
renderer-injected、profiling-hooks-status; - commit 周期:
commit、post-commit、fiber-unmount、commit-start/commit-stop、render-start/render-yield/render-stop/render-scheduled; - effect 边界:
layout-effects-start/stop、passive-effects-start/stop; - 组件级:
component-render-start/stop,各 layout / passive effect 的 mount/unmount 事件,state-update、force-update、component-suspended、component-errored。
其中两类字段值得注意:
commit事件:每次 commit 携带tree字段,是按 fiber 展开的LiteFiberSummary数组,每个条目含name、depth、tag、actualDuration、actualStartTime、selfBaseDuration、treeBaseDuration,以及启用对应选项后出现的fiberId、source/ownerName、changeDescription。事件还带priorityName(如"UserBlocking"),由 scheduler 优先级经getLaneLabelMap翻译得到;commit 中发生错误时会带didError: true。profiling-hooks-status事件:injectProfilingHooks成功时为available: true;失败时available: false,并带reason(no-inject-method/threw/opted-out)、reactVersion、bundleType。这是判断mark*类事件是否会触发的依据。
可选:为每个 fiber 附加重渲染原因
recordChangeDescriptions(默认false)开启后,每个 fiber 摘要附带changeDescription: { isFirstMount, props, state, context, hooks, parent },直接回答"这个组件为什么重渲染了"。各 per-fiber 选项及默认值(README 与 types.ts 一致):
| 选项 | 默认值 | 效果 |
|---|---|---|
recordChangeDescriptions | false | 每个 fiber 附带changeDescription,归因到 props / state / context / hooks / parent |
includeFiberSource | false | 附带source: { fileName, lineNumber, columnNumber }与ownerName,来自_debugSource(React 16/17/18)或_debugStack(React 19+),同步提取,无 source-map 解析 |
includeFiberIdentity | false | 附带稳定的单调递增fiberId(alternate-aware),可用于跨 commit 关联同一逻辑 fiber |
includeLaneLabels | true | 翻译laneLabels与priorityName |
maxFibersPerCommit | 5000 | 单个 commit 最多遍历的 fiber 数,超出截断以限制 payload |
minFiberActualDurationMs | 0 | 跳过actualDuration低于该阈值的 fiber 子树 |
注意:把includeFiberTree设为false会整体关闭 fiber 树遍历,此时三个 enrichment 选项全部失效,instrument()会打印一次性警告提示这一点。
可选:把事件流 POST 到 ingest 端点
给instrument()传endpoint后,每个事件会额外以fetchPOST 上报,请求体为{ sessionId, location, message, data, timestamp }(keepalive: true,Content-Type: application/json,实现见 create-emitter.ts):
const handle = instrument({ onEvent: (event) => console.log(event), // 可选:把每个事件上报到端点 endpoint: "http://127.0.0.1:54321/ingest/abc123", // 替换为你的接收地址 sessionId: "abc123", // 你的会话标识 });使用端点上报时的硬性规则:
endpoint必须同时提供sessionId,否则不发 POST,并打印一次警告:`[react-scan/lite] `endpoint` requires `sessionId`; events will not be POSTed.`;endpoint必须在instrument()时刻就是合法的 http/https URL,否则该 endpoint 被直接丢弃(后续事件不会逐条去 fetch 失败),并打印错误`[react-scan/lite] `endpoint` is not a valid http(s) URL; events will not be POSTed.`;keepalive请求体有约 64KB 上限,带大tree的 payload 可能被静默丢弃;location选项(默认"ReactScanLite")作为每次 POST 中location字段的前缀。
构建档位限制:actualDuration只在__PROFILE__构建中填充
actualDuration和mark*profiling hooks 只在__PROFILE__构建中有数据。开发环境默认满足;生产环境需要在打包器里把react-domalias 到react-dom/profiling(webpack/vite 的resolve.alias)。否则onCommitFiberRoot仍会触发commit事件,但每个 fiber 的actualDuration都是0。
此时profiling-hooks-status事件会给出明确信号:available: false且reason: "no-inject-method"表示 React 19.2+ 的生产构建或非__PROFILE__构建,mark*hooks 不会触发。README 建议的消费方式是:在这种情况下回退到long-animation-frame(LoAF)归因,而不是判定为"应用空闲"。
与 React DevTools 共存的行为
如果 React DevTools 同时已 attach,instrument()会打印一次性警告:injectProfilingHooks通道是硬性替换,插桩激活期间 DevTools 的 Timeline Profiler 收不到事件。调用handle.stop()会还原 hook 上的原始 handler(inject、onCommitFiberRoot、onPostCommitFiberRoot、onCommitFiberUnmount),DevTools 恢复接收。
同一页面内重复调用instrument()(未stop())会直接返回已有 handle,不会重复挂接。
验证插桩是否生效
按 lite.test.ts 中验证过的事件形态,在你的开发环境下可以这样核对:
- 调用
instrument()后立即检查handle.isActive()为true(浏览器环境;Node 环境下为false,属于 SSR noop 行为); - 在
profiling-hooks-status事件中确认available: true。开发构建下这是预期结果;若为available: false,按上面的reason值区分是生产/非__PROFILE__构建(no-inject-method)、注入抛错(threw,此时事件带error信息)还是主动关闭(opted-out); - 触发一次状态更新后,确认收到
kind === "commit"的事件,且tree数组包含组件名、depth、actualDuration等字段;开启recordChangeDescriptions时可进一步检查某条目的changeDescription(首次挂载时isFirstMount为true); - 调用
handle.stop()后再触发更新,确认不再收到 commit 事件,且isActive()为false;重复调用stop()不抛错。
subscribe返回的取消订阅函数只移除单个监听器,不影响其他监听者与 endpoint POST;彻底停止请用handle.stop()。
边界与下一步
mark*类事件(组件 render、effect 边界、state-update等)只在 React 16.5–19.1 的__PROFILE__构建中触发;changeDescription的hooks字段是近似值:按引用比较每个 hook 的memoizedState,useMemo/useCallback的变化也会被报告;host 节点等类型的 fiber 其changeDescription为null;includeFiberSource只做同步提取,没有 source-map symbolication,React 19+ 从_debugStack解析,fiber 缺少 debug 信息时source与ownerName为null。
事件流的完整类型定义(LiteEvent、LiteOptions、LiteHandle、ProfilingHooks)都在 packages/scan/src/lite/types.ts,插桩与 hook 替换逻辑在 packages/scan/src/lite/index.ts,fiber 遍历在 packages/scan/src/lite/walk-fiber.ts,可继续深入。
【免费下载链接】react-scanScan and fix React performance issues项目地址: https://gitcode.com/GitHub_Trending/re/react-scan
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考