@react-perfscope/react 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +117 -0
- package/dist/index.cjs +297 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +131 -0
- package/dist/index.d.ts +131 -0
- package/dist/index.js +265 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ray
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# @react-perfscope/react
|
|
2
|
+
|
|
3
|
+
React 18+ adapter for `react-perfscope`. Installs a DevTools global hook to observe commits, walks fiber trees, and exposes a render collector that plugs into `@react-perfscope/core`.
|
|
4
|
+
|
|
5
|
+
## Status
|
|
6
|
+
|
|
7
|
+
Phase 3-4 stable. Render collector emits one `RenderSignal` per changed component per commit; `RenderSignal.duration` populated from `fiber.actualDuration` when React is built with Profiling.
|
|
8
|
+
|
|
9
|
+
## Example
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { createRecorder } from '@react-perfscope/core'
|
|
13
|
+
import { createRenderCollector } from '@react-perfscope/react'
|
|
14
|
+
|
|
15
|
+
const recorder = createRecorder()
|
|
16
|
+
recorder.use(createRenderCollector())
|
|
17
|
+
|
|
18
|
+
recorder.start()
|
|
19
|
+
// ... interact with the app ...
|
|
20
|
+
const result = recorder.stop()
|
|
21
|
+
|
|
22
|
+
console.log(result.signals.filter((s) => s.kind === 'render'))
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## API
|
|
26
|
+
|
|
27
|
+
- `createRenderCollector()` — Collector factory. Emits `RenderSignal` per non-host fiber on each React commit.
|
|
28
|
+
- `resolveComponentFromElement(el)` — Given a DOM element, return the nearest React component name (or null if no fiber attached).
|
|
29
|
+
- `installDevToolsHook(listener)` — Low-level DevTools hook installer. Returns an unsubscribe function. Chains with any pre-existing hook (e.g. real React DevTools).
|
|
30
|
+
- `fiberComponentName(fiber)` — Resolve a fiber to its component name. Handles host tags, function/class components, `memo`, `forwardRef`.
|
|
31
|
+
- `walkChangedFibers(root, visit, { stopAt })` — Depth-first traversal of a fiber subtree with an upper bound.
|
|
32
|
+
|
|
33
|
+
## Hook load-order (IMPORTANT)
|
|
34
|
+
|
|
35
|
+
`react-dom` reads `globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__` ONCE at module evaluation time. If the hook isn't there at that moment, `react-dom`'s internal `injectedHook` is set to `null` and never updated — our collector will then never receive commits.
|
|
36
|
+
|
|
37
|
+
**Practical implication:** import `@react-perfscope/react` (or call `createRenderCollector()` / `installDevToolsHook()`) BEFORE you `import 'react-dom/client'` or before any module that does. The simplest pattern:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// At the very top of your entry file
|
|
41
|
+
import { createRecorder } from '@react-perfscope/core'
|
|
42
|
+
import { createRenderCollector } from '@react-perfscope/react'
|
|
43
|
+
|
|
44
|
+
const recorder = createRecorder()
|
|
45
|
+
recorder.use(createRenderCollector())
|
|
46
|
+
|
|
47
|
+
// Now import React-DOM-touching code
|
|
48
|
+
import './app'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
If you're using the `react-perfscope` meta package (or one of the build plugins), this ordering is handled automatically.
|
|
52
|
+
|
|
53
|
+
## Caveats
|
|
54
|
+
|
|
55
|
+
- The render collector keeps its DevTools hook listener attached across deactivate cycles (emission is gated by an `active` flag). This mirrors the `web-vitals` collector's lifecycle.
|
|
56
|
+
- `RenderSignal.duration` is `0` for fibers outside a Profiler-enabled root (React's default `createRoot` is Profiler-enabled in development).
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
<a id="한국어"></a>
|
|
61
|
+
|
|
62
|
+
# 한국어
|
|
63
|
+
|
|
64
|
+
`react-perfscope`용 React 18+ 어댑터. DevTools 글로벌 훅을 설치해 커밋을 감지하고, fiber 트리를 순회해서 `@react-perfscope/core`에 연결되는 render collector를 제공한다.
|
|
65
|
+
|
|
66
|
+
## 상태
|
|
67
|
+
|
|
68
|
+
Phase 3-4 안정화. render collector는 커밋마다 변경된 컴포넌트 하나당 `RenderSignal` 하나를 내보낸다. React가 Profiling 빌드일 경우 `RenderSignal.duration`은 `fiber.actualDuration`으로 채워진다.
|
|
69
|
+
|
|
70
|
+
## 예제
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { createRecorder } from '@react-perfscope/core'
|
|
74
|
+
import { createRenderCollector } from '@react-perfscope/react'
|
|
75
|
+
|
|
76
|
+
const recorder = createRecorder()
|
|
77
|
+
recorder.use(createRenderCollector())
|
|
78
|
+
|
|
79
|
+
recorder.start()
|
|
80
|
+
// ... 앱과 상호작용 ...
|
|
81
|
+
const result = recorder.stop()
|
|
82
|
+
|
|
83
|
+
console.log(result.signals.filter((s) => s.kind === 'render'))
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## API
|
|
87
|
+
|
|
88
|
+
- `createRenderCollector()` — Collector 팩토리. React 커밋마다 non-host fiber에 대해 `RenderSignal`을 내보낸다.
|
|
89
|
+
- `resolveComponentFromElement(el)` — DOM 엘리먼트를 받아 가장 가까운 React 컴포넌트 이름을 반환한다 (fiber가 없으면 null).
|
|
90
|
+
- `installDevToolsHook(listener)` — 저수준 DevTools 훅 설치 함수. unsubscribe 함수를 반환한다. 기존에 있던 훅(예: 실제 React DevTools)이 있으면 체이닝한다.
|
|
91
|
+
- `fiberComponentName(fiber)` — fiber를 컴포넌트 이름으로 해석한다. host 태그, 함수/클래스 컴포넌트, `memo`, `forwardRef`를 모두 처리한다.
|
|
92
|
+
- `walkChangedFibers(root, visit, { stopAt })` — 상한선을 두고 fiber 서브트리를 깊이 우선으로 순회한다.
|
|
93
|
+
|
|
94
|
+
## 훅 로드 순서 (중요)
|
|
95
|
+
|
|
96
|
+
`react-dom`은 모듈 평가 시점에 `globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__`을 딱 한 번 읽는다. 그 시점에 훅이 없으면 `react-dom` 내부의 `injectedHook`이 `null`로 고정되어 이후에 업데이트되지 않는다 — 그러면 collector가 커밋을 영원히 못 받는다.
|
|
97
|
+
|
|
98
|
+
**실용적인 의미:** `@react-perfscope/react`를 import하거나 `createRenderCollector()` / `installDevToolsHook()`를 호출하는 건 반드시 `import 'react-dom/client'`보다 먼저, 그리고 그걸 import하는 어떤 모듈보다도 먼저 와야 한다. 가장 간단한 패턴:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
// entry 파일 맨 위에
|
|
102
|
+
import { createRecorder } from '@react-perfscope/core'
|
|
103
|
+
import { createRenderCollector } from '@react-perfscope/react'
|
|
104
|
+
|
|
105
|
+
const recorder = createRecorder()
|
|
106
|
+
recorder.use(createRenderCollector())
|
|
107
|
+
|
|
108
|
+
// 이제 React-DOM을 건드리는 코드를 import
|
|
109
|
+
import './app'
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`react-perfscope` 메타 패키지(또는 빌드 플러그인 중 하나)를 쓰면 이 순서가 자동으로 처리된다.
|
|
113
|
+
|
|
114
|
+
## 주의사항
|
|
115
|
+
|
|
116
|
+
- render collector는 deactivate 사이클에도 DevTools 훅 리스너를 붙여둔다 (내보내기는 `active` 플래그로 제어된다). 이건 `web-vitals` collector의 생명주기와 같은 방식이다.
|
|
117
|
+
- Profiler가 활성화된 root 바깥에 있는 fiber는 `RenderSignal.duration`이 `0`이다 (개발 모드의 `createRoot`는 기본적으로 Profiler가 켜져 있다).
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
createRenderCollector: () => createRenderCollector,
|
|
24
|
+
fiberComponentName: () => fiberComponentName,
|
|
25
|
+
installDevToolsHook: () => installDevToolsHook,
|
|
26
|
+
resolveComponentFromElement: () => resolveComponentFromElement,
|
|
27
|
+
uninstallDevToolsHook: () => uninstallDevToolsHook,
|
|
28
|
+
walkChangedFibers: () => walkChangedFibers
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(index_exports);
|
|
31
|
+
|
|
32
|
+
// src/devtools-hook.ts
|
|
33
|
+
var HOOK_KEY = "__REACT_DEVTOOLS_GLOBAL_HOOK__";
|
|
34
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
35
|
+
var ourHook = null;
|
|
36
|
+
var chainedOriginal = null;
|
|
37
|
+
function ensureHookInstalled() {
|
|
38
|
+
const g = globalThis;
|
|
39
|
+
if (ourHook && g[HOOK_KEY] === ourHook) return;
|
|
40
|
+
const existing = g[HOOK_KEY];
|
|
41
|
+
chainedOriginal = existing && existing !== ourHook && typeof existing.onCommitFiberRoot === "function" ? existing.onCommitFiberRoot : null;
|
|
42
|
+
const hook = existing && existing !== ourHook ? existing : {};
|
|
43
|
+
if (!hook.supportsFiber) {
|
|
44
|
+
hook.supportsFiber = true;
|
|
45
|
+
}
|
|
46
|
+
if (typeof hook.inject !== "function") {
|
|
47
|
+
let _nextRendererId = 1;
|
|
48
|
+
hook.inject = () => _nextRendererId++;
|
|
49
|
+
}
|
|
50
|
+
if (!hook.renderers) {
|
|
51
|
+
;
|
|
52
|
+
hook.renderers = /* @__PURE__ */ new Map();
|
|
53
|
+
}
|
|
54
|
+
if (typeof hook["on"] !== "function") {
|
|
55
|
+
;
|
|
56
|
+
hook["on"] = () => {
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (typeof hook["off"] !== "function") {
|
|
60
|
+
;
|
|
61
|
+
hook["off"] = () => {
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (typeof hook["emit"] !== "function") {
|
|
65
|
+
;
|
|
66
|
+
hook["emit"] = () => {
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (typeof hook["sub"] !== "function") {
|
|
70
|
+
;
|
|
71
|
+
hook["sub"] = () => () => {
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (typeof hook["checkDCE"] !== "function") {
|
|
75
|
+
;
|
|
76
|
+
hook["checkDCE"] = () => {
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (typeof hook["onCommitFiberUnmount"] !== "function") {
|
|
80
|
+
;
|
|
81
|
+
hook["onCommitFiberUnmount"] = () => {
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (typeof hook["onPostCommitFiberRoot"] !== "function") {
|
|
85
|
+
;
|
|
86
|
+
hook["onPostCommitFiberRoot"] = () => {
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
hook.onCommitFiberRoot = (rendererId, root, priorityLevel) => {
|
|
90
|
+
if (chainedOriginal) {
|
|
91
|
+
try {
|
|
92
|
+
chainedOriginal(rendererId, root, priorityLevel);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.warn("[react-perfscope] chained DevTools hook threw:", err);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
for (const cb of listeners) {
|
|
98
|
+
try {
|
|
99
|
+
cb(root, rendererId);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.warn("[react-perfscope] commit listener threw:", err);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
g[HOOK_KEY] = hook;
|
|
106
|
+
ourHook = hook;
|
|
107
|
+
}
|
|
108
|
+
function installDevToolsHook(listener) {
|
|
109
|
+
ensureHookInstalled();
|
|
110
|
+
listeners.add(listener);
|
|
111
|
+
return () => {
|
|
112
|
+
listeners.delete(listener);
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function uninstallDevToolsHook() {
|
|
116
|
+
listeners.clear();
|
|
117
|
+
ourHook = null;
|
|
118
|
+
chainedOriginal = null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/fiber-walker.ts
|
|
122
|
+
var MEMO_TYPE = /* @__PURE__ */ Symbol.for("react.memo");
|
|
123
|
+
var FORWARD_REF_TYPE = /* @__PURE__ */ Symbol.for("react.forward_ref");
|
|
124
|
+
function namedFunctionName(value) {
|
|
125
|
+
if (typeof value !== "function") return null;
|
|
126
|
+
const fn = value;
|
|
127
|
+
if (typeof fn.displayName === "string" && fn.displayName.length > 0) return fn.displayName;
|
|
128
|
+
if (typeof fn.name === "string" && fn.name.length > 0) return fn.name;
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
function fiberComponentName(fiber) {
|
|
132
|
+
if (!fiber) return null;
|
|
133
|
+
const type = fiber.type;
|
|
134
|
+
if (typeof type === "string") return type;
|
|
135
|
+
if (typeof type === "function") return namedFunctionName(type);
|
|
136
|
+
if (type && typeof type === "object") {
|
|
137
|
+
const wrapper = type;
|
|
138
|
+
if (wrapper.$$typeof === MEMO_TYPE) {
|
|
139
|
+
return namedFunctionName(wrapper.type);
|
|
140
|
+
}
|
|
141
|
+
if (wrapper.$$typeof === FORWARD_REF_TYPE) {
|
|
142
|
+
return namedFunctionName(wrapper.render);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
function walkChangedFibers(root, visit, opts = {}) {
|
|
148
|
+
const max = opts.stopAt ?? 1e4;
|
|
149
|
+
const descend = opts.descend ?? (() => true);
|
|
150
|
+
let count = 0;
|
|
151
|
+
let depth = 0;
|
|
152
|
+
let node = root;
|
|
153
|
+
while (node) {
|
|
154
|
+
visit(node, depth);
|
|
155
|
+
count++;
|
|
156
|
+
if (count >= max) return;
|
|
157
|
+
if (node.child && descend(node)) {
|
|
158
|
+
node = node.child;
|
|
159
|
+
depth++;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
while (node && !node.sibling) {
|
|
163
|
+
if (node === root) return;
|
|
164
|
+
node = node.return;
|
|
165
|
+
depth--;
|
|
166
|
+
}
|
|
167
|
+
if (!node || node === root) return;
|
|
168
|
+
node = node.sibling;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/attribution.ts
|
|
173
|
+
var FIBER_KEY_PATTERNS = ["__reactFiber$", "__reactInternalInstance$"];
|
|
174
|
+
function findFiberOnElement(el) {
|
|
175
|
+
for (const key of Object.keys(el)) {
|
|
176
|
+
for (const pattern of FIBER_KEY_PATTERNS) {
|
|
177
|
+
if (key.startsWith(pattern)) {
|
|
178
|
+
return el[key] ?? null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
function resolveComponentFromElement(el) {
|
|
185
|
+
const start = findFiberOnElement(el);
|
|
186
|
+
if (!start) return null;
|
|
187
|
+
let node = start;
|
|
188
|
+
while (node) {
|
|
189
|
+
if (typeof node.type === "function" || node.type && typeof node.type === "object") {
|
|
190
|
+
const name = fiberComponentName(node);
|
|
191
|
+
if (name) return name;
|
|
192
|
+
}
|
|
193
|
+
node = node.return;
|
|
194
|
+
}
|
|
195
|
+
return fiberComponentName(start);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/render-reason.ts
|
|
199
|
+
var PERFORMED_WORK = 1;
|
|
200
|
+
function didPerformWork(fiber) {
|
|
201
|
+
if (!fiber || typeof fiber.flags !== "number") return false;
|
|
202
|
+
return (fiber.flags & PERFORMED_WORK) !== 0;
|
|
203
|
+
}
|
|
204
|
+
function subtreeMightHaveRendered(fiber) {
|
|
205
|
+
if (typeof fiber.subtreeFlags !== "number") return true;
|
|
206
|
+
return (fiber.subtreeFlags & PERFORMED_WORK) !== 0;
|
|
207
|
+
}
|
|
208
|
+
function nearestComponentAncestor(fiber) {
|
|
209
|
+
let p = fiber.return;
|
|
210
|
+
while (p) {
|
|
211
|
+
if (typeof p.type === "function") return p;
|
|
212
|
+
p = p.return;
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
function asObject(value) {
|
|
217
|
+
return value && typeof value === "object" ? value : {};
|
|
218
|
+
}
|
|
219
|
+
function changedPropKeys(prev, next) {
|
|
220
|
+
if (prev === next) return [];
|
|
221
|
+
const p = asObject(prev);
|
|
222
|
+
const n = asObject(next);
|
|
223
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(p), ...Object.keys(n)]);
|
|
224
|
+
const changed = [];
|
|
225
|
+
for (const k of keys) {
|
|
226
|
+
if (p[k] !== n[k]) changed.push(k);
|
|
227
|
+
}
|
|
228
|
+
return changed;
|
|
229
|
+
}
|
|
230
|
+
function classifyRenderReason(fiber) {
|
|
231
|
+
if (!fiber.alternate) return { reason: "mount" };
|
|
232
|
+
const changed = changedPropKeys(fiber.alternate.memoizedProps, fiber.memoizedProps);
|
|
233
|
+
if (changed.length > 0) return { reason: "props", changedProps: changed };
|
|
234
|
+
if (didPerformWork(nearestComponentAncestor(fiber))) return { reason: "parent" };
|
|
235
|
+
return { reason: "state" };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/render-collector.ts
|
|
239
|
+
function createRenderCollector() {
|
|
240
|
+
let active = false;
|
|
241
|
+
let emit = () => {
|
|
242
|
+
};
|
|
243
|
+
let unsubscribe = null;
|
|
244
|
+
let commitId = 0;
|
|
245
|
+
function onCommit(root) {
|
|
246
|
+
if (!active) return;
|
|
247
|
+
const at = performance.now();
|
|
248
|
+
const id = commitId++;
|
|
249
|
+
walkChangedFibers(
|
|
250
|
+
root.current,
|
|
251
|
+
(fiber, depth) => {
|
|
252
|
+
if (typeof fiber.type === "string") return;
|
|
253
|
+
if (!didPerformWork(fiber)) return;
|
|
254
|
+
const name = fiberComponentName(fiber);
|
|
255
|
+
if (!name) return;
|
|
256
|
+
const duration = typeof fiber.actualDuration === "number" ? fiber.actualDuration : 0;
|
|
257
|
+
const { reason, changedProps } = classifyRenderReason(fiber);
|
|
258
|
+
emit({
|
|
259
|
+
kind: "render",
|
|
260
|
+
at,
|
|
261
|
+
component: name,
|
|
262
|
+
reason,
|
|
263
|
+
duration,
|
|
264
|
+
commitId: id,
|
|
265
|
+
depth,
|
|
266
|
+
...changedProps ? { changedProps } : {}
|
|
267
|
+
});
|
|
268
|
+
},
|
|
269
|
+
// Prune subtrees that did no work this commit. Without this, a leaf that
|
|
270
|
+
// mounted long ago keeps its stale PerformedWork flag and gets reported
|
|
271
|
+
// as a phantom render on every unrelated commit.
|
|
272
|
+
{ descend: subtreeMightHaveRendered }
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
kind: "render",
|
|
277
|
+
activate(emitFn) {
|
|
278
|
+
emit = emitFn;
|
|
279
|
+
active = true;
|
|
280
|
+
if (unsubscribe) return;
|
|
281
|
+
unsubscribe = installDevToolsHook(onCommit);
|
|
282
|
+
},
|
|
283
|
+
deactivate() {
|
|
284
|
+
active = false;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
289
|
+
0 && (module.exports = {
|
|
290
|
+
createRenderCollector,
|
|
291
|
+
fiberComponentName,
|
|
292
|
+
installDevToolsHook,
|
|
293
|
+
resolveComponentFromElement,
|
|
294
|
+
uninstallDevToolsHook,
|
|
295
|
+
walkChangedFibers
|
|
296
|
+
});
|
|
297
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/devtools-hook.ts","../src/fiber-walker.ts","../src/attribution.ts","../src/render-reason.ts","../src/render-collector.ts"],"sourcesContent":["export * from './types'\nexport { installDevToolsHook, uninstallDevToolsHook } from './devtools-hook'\nexport { fiberComponentName, walkChangedFibers } from './fiber-walker'\nexport { resolveComponentFromElement } from './attribution'\nexport { createRenderCollector } from './render-collector'\n","import type { MinimalFiber, ReactDevToolsHook } from './types'\n\ntype CommitListener = (root: { current: MinimalFiber }, rendererId: number) => void\n\nconst HOOK_KEY = '__REACT_DEVTOOLS_GLOBAL_HOOK__'\n\ninterface GlobalWithHook {\n [HOOK_KEY]?: ReactDevToolsHook\n}\n\nconst listeners = new Set<CommitListener>()\nlet ourHook: ReactDevToolsHook | null = null\nlet chainedOriginal: ReactDevToolsHook['onCommitFiberRoot'] | null = null\n\nfunction ensureHookInstalled(): void {\n const g = globalThis as GlobalWithHook\n // Already ours and still installed — nothing to do.\n if (ourHook && g[HOOK_KEY] === ourHook) return\n\n const existing = g[HOOK_KEY]\n chainedOriginal =\n existing && existing !== ourHook && typeof existing.onCommitFiberRoot === 'function'\n ? existing.onCommitFiberRoot\n : null\n\n const hook: ReactDevToolsHook = existing && existing !== ourHook ? existing : {}\n // React's injectInternals() checks for supportsFiber and calls inject() to\n // get a renderer ID. Without these, React stores injectedHook = null and\n // never calls onCommitFiberRoot on subsequent commits.\n if (!hook.supportsFiber) {\n hook.supportsFiber = true\n }\n if (typeof hook.inject !== 'function') {\n let _nextRendererId = 1\n hook.inject = () => _nextRendererId++\n }\n // Additional fields that React Refresh (@vitejs/plugin-react) accesses on\n // the hook. Without these it throws \"Cannot read properties of undefined\n // (reading 'forEach')\" at injectIntoGlobalHook. We default each to a safe\n // empty/noop value; if the real React DevTools extension installs later\n // it will overwrite these.\n if (!hook.renderers) {\n ;(hook as Record<string, unknown>).renderers = new Map()\n }\n if (typeof (hook as Record<string, unknown>)['on'] !== 'function') {\n ;(hook as Record<string, unknown>)['on'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['off'] !== 'function') {\n ;(hook as Record<string, unknown>)['off'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['emit'] !== 'function') {\n ;(hook as Record<string, unknown>)['emit'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['sub'] !== 'function') {\n ;(hook as Record<string, unknown>)['sub'] = () => () => {}\n }\n if (typeof (hook as Record<string, unknown>)['checkDCE'] !== 'function') {\n ;(hook as Record<string, unknown>)['checkDCE'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['onCommitFiberUnmount'] !== 'function') {\n ;(hook as Record<string, unknown>)['onCommitFiberUnmount'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['onPostCommitFiberRoot'] !== 'function') {\n ;(hook as Record<string, unknown>)['onPostCommitFiberRoot'] = () => {}\n }\n hook.onCommitFiberRoot = (rendererId, root, priorityLevel) => {\n if (chainedOriginal) {\n try {\n chainedOriginal(rendererId, root, priorityLevel)\n } catch (err) {\n console.warn('[react-perfscope] chained DevTools hook threw:', err)\n }\n }\n for (const cb of listeners) {\n try {\n cb(root, rendererId)\n } catch (err) {\n console.warn('[react-perfscope] commit listener threw:', err)\n }\n }\n }\n g[HOOK_KEY] = hook\n ourHook = hook\n}\n\nexport function installDevToolsHook(listener: CommitListener): () => void {\n ensureHookInstalled()\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\n/**\n * Clears all listeners and forgets our installed hook reference. Used by\n * tests to fully reset module state. Does NOT remove the hook from\n * globalThis — callers that want a fully clean slate must also\n * `delete globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__`.\n */\nexport function uninstallDevToolsHook(): void {\n listeners.clear()\n ourHook = null\n chainedOriginal = null\n}\n","import type { MinimalFiber } from './types'\n\nconst MEMO_TYPE = Symbol.for('react.memo')\nconst FORWARD_REF_TYPE = Symbol.for('react.forward_ref')\n\ntype WrapperType = {\n $$typeof: symbol\n type?: unknown\n render?: unknown\n}\n\nfunction namedFunctionName(value: unknown): string | null {\n if (typeof value !== 'function') return null\n const fn = value as { displayName?: string; name?: string }\n if (typeof fn.displayName === 'string' && fn.displayName.length > 0) return fn.displayName\n if (typeof fn.name === 'string' && fn.name.length > 0) return fn.name\n return null\n}\n\n/**\n * Return the component name for a fiber.\n *\n * Host components (DOM tags) return their tag string. Function and class\n * components return their displayName or name. Memo and forwardRef wrappers\n * are unwrapped to their inner component. Unknown shapes return null.\n */\nexport function fiberComponentName(fiber: MinimalFiber | null): string | null {\n if (!fiber) return null\n const type = fiber.type\n if (typeof type === 'string') return type\n if (typeof type === 'function') return namedFunctionName(type)\n if (type && typeof type === 'object') {\n const wrapper = type as WrapperType\n if (wrapper.$$typeof === MEMO_TYPE) {\n return namedFunctionName(wrapper.type)\n }\n if (wrapper.$$typeof === FORWARD_REF_TYPE) {\n return namedFunctionName(wrapper.render)\n }\n }\n return null\n}\n\ninterface WalkOptions {\n /**\n * Maximum number of fibers to visit. Prevents runaway traversal on very\n * deep trees. Default 10_000.\n */\n stopAt?: number\n /**\n * Predicate deciding whether to descend into a fiber's children. Returning\n * false prunes the entire subtree (the fiber itself is still visited). Used\n * to skip subtrees that did no work this commit. Defaults to always descend.\n */\n descend?: (fiber: MinimalFiber) => boolean\n}\n\n/**\n * Walk a fiber subtree depth-first, invoking `visit` for every fiber with its\n * depth relative to `root` (root is depth 0). Returns early once `stopAt`\n * fibers have been visited. When `descend` returns false for a fiber, its\n * children are skipped entirely.\n */\nexport function walkChangedFibers(\n root: MinimalFiber,\n visit: (fiber: MinimalFiber, depth: number) => void,\n opts: WalkOptions = {}\n): void {\n const max = opts.stopAt ?? 10_000\n const descend = opts.descend ?? (() => true)\n let count = 0\n let depth = 0\n let node: MinimalFiber | null = root\n while (node) {\n visit(node, depth)\n count++\n if (count >= max) return\n if (node.child && descend(node)) {\n node = node.child\n depth++\n continue\n }\n while (node && !node.sibling) {\n if (node === root) return\n node = node.return\n depth--\n }\n if (!node || node === root) return\n node = node.sibling\n }\n}\n","import type { MinimalFiber } from './types'\nimport { fiberComponentName } from './fiber-walker'\n\nconst FIBER_KEY_PATTERNS = ['__reactFiber$', '__reactInternalInstance$']\n\nfunction findFiberOnElement(el: HTMLElement): MinimalFiber | null {\n for (const key of Object.keys(el)) {\n for (const pattern of FIBER_KEY_PATTERNS) {\n if (key.startsWith(pattern)) {\n return (el as HTMLElement & Record<string, MinimalFiber>)[key] ?? null\n }\n }\n }\n return null\n}\n\n/**\n * Walk up from the fiber attached to `el` until we find one whose `type` is\n * a function or class component (not a host tag string). Returns that\n * component's display name. If no component is found, returns the host\n * tag name of the starting fiber. If no fiber is attached, returns null.\n */\nexport function resolveComponentFromElement(el: HTMLElement): string | null {\n const start = findFiberOnElement(el)\n if (!start) return null\n let node: MinimalFiber | null = start\n while (node) {\n if (typeof node.type === 'function' || (node.type && typeof node.type === 'object')) {\n const name = fiberComponentName(node)\n if (name) return name\n }\n node = node.return\n }\n // Nothing but host fibers above — return the host tag name of the starting fiber.\n return fiberComponentName(start)\n}\n","import type { MinimalFiber } from './types'\n\n/** React's `PerformedWork` flag — set on a fiber whose render actually ran. */\nexport const PERFORMED_WORK = 0b1\n\nexport type RenderReason = 'mount' | 'state' | 'props' | 'parent'\n\nexport interface RenderReasonResult {\n reason: RenderReason\n changedProps?: string[]\n}\n\nexport function didPerformWork(fiber: MinimalFiber | null): boolean {\n if (!fiber || typeof fiber.flags !== 'number') return false\n return (fiber.flags & PERFORMED_WORK) !== 0\n}\n\n/**\n * Should the walk descend into this fiber's children when hunting for renders?\n *\n * React bubbles the `PerformedWork` bit of every descendant into a fiber's\n * `subtreeFlags`, and (unlike `flags`) resets it whenever the reconciler\n * re-clones the fiber. So a numeric `subtreeFlags` without the bit reliably\n * means \"no descendant rendered this commit\" — we can stop here and avoid\n * stale leaves whose own `flags` never got cleared.\n *\n * When `subtreeFlags` is absent (React < 18) we cannot prune safely, so we\n * descend and fall back to the full-tree walk.\n */\nexport function subtreeMightHaveRendered(fiber: MinimalFiber): boolean {\n if (typeof fiber.subtreeFlags !== 'number') return true\n return (fiber.subtreeFlags & PERFORMED_WORK) !== 0\n}\n\n/**\n * Walk up `.return` to the nearest *component* fiber, skipping host (DOM)\n * fibers. A component's immediate `.return` is usually a host fiber (the\n * `<div>` it's nested in), not its parent component — so to ask \"did my\n * parent component render?\" we have to climb past the host nodes first.\n */\nexport function nearestComponentAncestor(fiber: MinimalFiber): MinimalFiber | null {\n let p = fiber.return\n while (p) {\n if (typeof p.type === 'function') return p\n p = p.return\n }\n return null\n}\n\nfunction asObject(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' ? (value as Record<string, unknown>) : {}\n}\n\n/** Shallow diff of two prop bags. Returns the keys whose values differ. */\nexport function changedPropKeys(prev: unknown, next: unknown): string[] {\n if (prev === next) return []\n const p = asObject(prev)\n const n = asObject(next)\n const keys = new Set([...Object.keys(p), ...Object.keys(n)])\n const changed: string[] = []\n for (const k of keys) {\n if (p[k] !== n[k]) changed.push(k)\n }\n return changed\n}\n\n/**\n * Classify WHY a fiber that performed work this commit rendered.\n *\n * - `mount` — first render (no previous fiber).\n * - `props` — at least one prop changed since the previous render.\n * - `state` — props are identical AND the parent did not render, so the\n * update originated here (own state/force update). This is the\n * root of a render cascade.\n * - `parent` — props are identical but the parent rendered, so this only\n * re-rendered because of its parent. This is the avoidable\n * \"wrap me in memo\" case the developer is hunting for.\n *\n * Assumes the caller only invokes this for fibers that actually performed\n * work (see `didPerformWork`).\n */\nexport function classifyRenderReason(fiber: MinimalFiber): RenderReasonResult {\n if (!fiber.alternate) return { reason: 'mount' }\n const changed = changedPropKeys(fiber.alternate.memoizedProps, fiber.memoizedProps)\n if (changed.length > 0) return { reason: 'props', changedProps: changed }\n if (didPerformWork(nearestComponentAncestor(fiber))) return { reason: 'parent' }\n return { reason: 'state' }\n}\n","import type { Collector, Signal } from '@react-perfscope/core'\nimport { installDevToolsHook } from './devtools-hook'\nimport { fiberComponentName, walkChangedFibers } from './fiber-walker'\nimport { classifyRenderReason, didPerformWork, subtreeMightHaveRendered } from './render-reason'\nimport type { MinimalFiber } from './types'\n\nexport function createRenderCollector(): Collector {\n let active = false\n let emit: (signal: Signal) => void = () => {}\n let unsubscribe: (() => void) | null = null\n let commitId = 0\n\n function onCommit(root: { current: MinimalFiber }) {\n if (!active) return\n const at = performance.now()\n const id = commitId++\n walkChangedFibers(\n root.current,\n (fiber, depth) => {\n // Skip host (DOM) fibers — we only want function/class components in\n // render reports.\n if (typeof fiber.type === 'string') return\n // Only report fibers that actually re-ran their render this commit.\n // A bailed-out fiber (e.g. memo with equal props) keeps its place in\n // the tree but did no work, so reporting it would be noise.\n if (!didPerformWork(fiber)) return\n const name = fiberComponentName(fiber)\n if (!name) return\n const duration = typeof fiber.actualDuration === 'number' ? fiber.actualDuration : 0\n const { reason, changedProps } = classifyRenderReason(fiber)\n emit({\n kind: 'render',\n at,\n component: name,\n reason,\n duration,\n commitId: id,\n depth,\n ...(changedProps ? { changedProps } : {}),\n })\n },\n // Prune subtrees that did no work this commit. Without this, a leaf that\n // mounted long ago keeps its stale PerformedWork flag and gets reported\n // as a phantom render on every unrelated commit.\n { descend: subtreeMightHaveRendered }\n )\n }\n\n return {\n kind: 'render',\n activate(emitFn) {\n emit = emitFn\n active = true\n if (unsubscribe) return\n unsubscribe = installDevToolsHook(onCommit)\n },\n deactivate() {\n active = false\n // Keep the global hook listener attached — installDevToolsHook is\n // idempotent and removing it on every deactivate would cost us the\n // ability to re-attach cleanly under the test reset hooks. The\n // `active` flag gates emission.\n },\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/// <reference lib="es2015" />
|
|
2
|
+
/// <reference lib="dom" />
|
|
3
|
+
|
|
4
|
+
import { Collector } from '@react-perfscope/core';
|
|
5
|
+
export { Collector } from '@react-perfscope/core';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolves a DOM element back to the nearest React component name in the
|
|
9
|
+
* fiber tree. Returns null when no React fiber is attached (e.g. host nodes
|
|
10
|
+
* outside any React root, detached nodes, or before React mounts).
|
|
11
|
+
*/
|
|
12
|
+
interface ReactAdapter {
|
|
13
|
+
/**
|
|
14
|
+
* Install the DevTools global hook to observe React commits. Idempotent:
|
|
15
|
+
* a second install is a no-op (or chains with an existing hook installed
|
|
16
|
+
* by React DevTools).
|
|
17
|
+
*/
|
|
18
|
+
install(): void;
|
|
19
|
+
/**
|
|
20
|
+
* Look up the component name for a DOM element by walking its fiber.
|
|
21
|
+
*/
|
|
22
|
+
resolveComponentFromElement(el: HTMLElement): string | null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Minimal shape of a React fiber node that we touch. The real fiber has
|
|
26
|
+
* many more fields; we only declare what we read.
|
|
27
|
+
*/
|
|
28
|
+
interface MinimalFiber {
|
|
29
|
+
stateNode: unknown;
|
|
30
|
+
type: unknown;
|
|
31
|
+
return: MinimalFiber | null;
|
|
32
|
+
child: MinimalFiber | null;
|
|
33
|
+
sibling: MinimalFiber | null;
|
|
34
|
+
alternate: MinimalFiber | null;
|
|
35
|
+
elementType?: unknown;
|
|
36
|
+
memoizedProps?: unknown;
|
|
37
|
+
/** Set by React when the fiber is inside a Profiler-enabled root. */
|
|
38
|
+
actualDuration?: number;
|
|
39
|
+
/**
|
|
40
|
+
* React's effect/work bitmask. The low bit (`PerformedWork`, 0b1) is set
|
|
41
|
+
* when beginWork actually ran this fiber's render (i.e. it did NOT bail
|
|
42
|
+
* out). We use it to tell "this rendered" from "this was skipped".
|
|
43
|
+
*
|
|
44
|
+
* Caveat: React does NOT clear this on a fiber that bailed out, so a leaf
|
|
45
|
+
* that rendered in some past commit keeps the bit set forever. Read it only
|
|
46
|
+
* for fibers the reconciler actually visited this commit (see `subtreeFlags`).
|
|
47
|
+
*/
|
|
48
|
+
flags?: number;
|
|
49
|
+
/**
|
|
50
|
+
* React bubbles every descendant's flags (including `PerformedWork`) into
|
|
51
|
+
* this field during completeWork. Unlike `flags`, it is reset whenever the
|
|
52
|
+
* reconciler re-clones a fiber, so `subtreeFlags & PerformedWork === 0`
|
|
53
|
+
* reliably means "nothing below me rendered this commit" — letting us prune
|
|
54
|
+
* the walk before reaching stale leaves. Absent on React < 18.
|
|
55
|
+
*/
|
|
56
|
+
subtreeFlags?: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The DevTools global hook React looks for at module load time. We register
|
|
60
|
+
* our own listener via `onCommitFiberRoot`.
|
|
61
|
+
*/
|
|
62
|
+
interface ReactDevToolsHook {
|
|
63
|
+
onCommitFiberRoot?: (rendererId: number, root: {
|
|
64
|
+
current: MinimalFiber;
|
|
65
|
+
}, priorityLevel?: unknown) => void;
|
|
66
|
+
/**
|
|
67
|
+
* React checks supportsFiber before storing the hook reference in its
|
|
68
|
+
* internal injectedHook. Must be true for React ≥ 16.4 to recognise the
|
|
69
|
+
* hook and call onCommitFiberRoot.
|
|
70
|
+
*/
|
|
71
|
+
supportsFiber?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* React calls inject(internals) and stores the returned renderer ID.
|
|
74
|
+
* A minimal no-op implementation (return 1) is sufficient for our purposes.
|
|
75
|
+
*/
|
|
76
|
+
inject?: (internals: unknown) => number;
|
|
77
|
+
[key: string]: unknown;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type CommitListener = (root: {
|
|
81
|
+
current: MinimalFiber;
|
|
82
|
+
}, rendererId: number) => void;
|
|
83
|
+
declare function installDevToolsHook(listener: CommitListener): () => void;
|
|
84
|
+
/**
|
|
85
|
+
* Clears all listeners and forgets our installed hook reference. Used by
|
|
86
|
+
* tests to fully reset module state. Does NOT remove the hook from
|
|
87
|
+
* globalThis — callers that want a fully clean slate must also
|
|
88
|
+
* `delete globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__`.
|
|
89
|
+
*/
|
|
90
|
+
declare function uninstallDevToolsHook(): void;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Return the component name for a fiber.
|
|
94
|
+
*
|
|
95
|
+
* Host components (DOM tags) return their tag string. Function and class
|
|
96
|
+
* components return their displayName or name. Memo and forwardRef wrappers
|
|
97
|
+
* are unwrapped to their inner component. Unknown shapes return null.
|
|
98
|
+
*/
|
|
99
|
+
declare function fiberComponentName(fiber: MinimalFiber | null): string | null;
|
|
100
|
+
interface WalkOptions {
|
|
101
|
+
/**
|
|
102
|
+
* Maximum number of fibers to visit. Prevents runaway traversal on very
|
|
103
|
+
* deep trees. Default 10_000.
|
|
104
|
+
*/
|
|
105
|
+
stopAt?: number;
|
|
106
|
+
/**
|
|
107
|
+
* Predicate deciding whether to descend into a fiber's children. Returning
|
|
108
|
+
* false prunes the entire subtree (the fiber itself is still visited). Used
|
|
109
|
+
* to skip subtrees that did no work this commit. Defaults to always descend.
|
|
110
|
+
*/
|
|
111
|
+
descend?: (fiber: MinimalFiber) => boolean;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Walk a fiber subtree depth-first, invoking `visit` for every fiber with its
|
|
115
|
+
* depth relative to `root` (root is depth 0). Returns early once `stopAt`
|
|
116
|
+
* fibers have been visited. When `descend` returns false for a fiber, its
|
|
117
|
+
* children are skipped entirely.
|
|
118
|
+
*/
|
|
119
|
+
declare function walkChangedFibers(root: MinimalFiber, visit: (fiber: MinimalFiber, depth: number) => void, opts?: WalkOptions): void;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Walk up from the fiber attached to `el` until we find one whose `type` is
|
|
123
|
+
* a function or class component (not a host tag string). Returns that
|
|
124
|
+
* component's display name. If no component is found, returns the host
|
|
125
|
+
* tag name of the starting fiber. If no fiber is attached, returns null.
|
|
126
|
+
*/
|
|
127
|
+
declare function resolveComponentFromElement(el: HTMLElement): string | null;
|
|
128
|
+
|
|
129
|
+
declare function createRenderCollector(): Collector;
|
|
130
|
+
|
|
131
|
+
export { type MinimalFiber, type ReactAdapter, type ReactDevToolsHook, createRenderCollector, fiberComponentName, installDevToolsHook, resolveComponentFromElement, uninstallDevToolsHook, walkChangedFibers };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/// <reference lib="es2015" />
|
|
2
|
+
/// <reference lib="dom" />
|
|
3
|
+
|
|
4
|
+
import { Collector } from '@react-perfscope/core';
|
|
5
|
+
export { Collector } from '@react-perfscope/core';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolves a DOM element back to the nearest React component name in the
|
|
9
|
+
* fiber tree. Returns null when no React fiber is attached (e.g. host nodes
|
|
10
|
+
* outside any React root, detached nodes, or before React mounts).
|
|
11
|
+
*/
|
|
12
|
+
interface ReactAdapter {
|
|
13
|
+
/**
|
|
14
|
+
* Install the DevTools global hook to observe React commits. Idempotent:
|
|
15
|
+
* a second install is a no-op (or chains with an existing hook installed
|
|
16
|
+
* by React DevTools).
|
|
17
|
+
*/
|
|
18
|
+
install(): void;
|
|
19
|
+
/**
|
|
20
|
+
* Look up the component name for a DOM element by walking its fiber.
|
|
21
|
+
*/
|
|
22
|
+
resolveComponentFromElement(el: HTMLElement): string | null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Minimal shape of a React fiber node that we touch. The real fiber has
|
|
26
|
+
* many more fields; we only declare what we read.
|
|
27
|
+
*/
|
|
28
|
+
interface MinimalFiber {
|
|
29
|
+
stateNode: unknown;
|
|
30
|
+
type: unknown;
|
|
31
|
+
return: MinimalFiber | null;
|
|
32
|
+
child: MinimalFiber | null;
|
|
33
|
+
sibling: MinimalFiber | null;
|
|
34
|
+
alternate: MinimalFiber | null;
|
|
35
|
+
elementType?: unknown;
|
|
36
|
+
memoizedProps?: unknown;
|
|
37
|
+
/** Set by React when the fiber is inside a Profiler-enabled root. */
|
|
38
|
+
actualDuration?: number;
|
|
39
|
+
/**
|
|
40
|
+
* React's effect/work bitmask. The low bit (`PerformedWork`, 0b1) is set
|
|
41
|
+
* when beginWork actually ran this fiber's render (i.e. it did NOT bail
|
|
42
|
+
* out). We use it to tell "this rendered" from "this was skipped".
|
|
43
|
+
*
|
|
44
|
+
* Caveat: React does NOT clear this on a fiber that bailed out, so a leaf
|
|
45
|
+
* that rendered in some past commit keeps the bit set forever. Read it only
|
|
46
|
+
* for fibers the reconciler actually visited this commit (see `subtreeFlags`).
|
|
47
|
+
*/
|
|
48
|
+
flags?: number;
|
|
49
|
+
/**
|
|
50
|
+
* React bubbles every descendant's flags (including `PerformedWork`) into
|
|
51
|
+
* this field during completeWork. Unlike `flags`, it is reset whenever the
|
|
52
|
+
* reconciler re-clones a fiber, so `subtreeFlags & PerformedWork === 0`
|
|
53
|
+
* reliably means "nothing below me rendered this commit" — letting us prune
|
|
54
|
+
* the walk before reaching stale leaves. Absent on React < 18.
|
|
55
|
+
*/
|
|
56
|
+
subtreeFlags?: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The DevTools global hook React looks for at module load time. We register
|
|
60
|
+
* our own listener via `onCommitFiberRoot`.
|
|
61
|
+
*/
|
|
62
|
+
interface ReactDevToolsHook {
|
|
63
|
+
onCommitFiberRoot?: (rendererId: number, root: {
|
|
64
|
+
current: MinimalFiber;
|
|
65
|
+
}, priorityLevel?: unknown) => void;
|
|
66
|
+
/**
|
|
67
|
+
* React checks supportsFiber before storing the hook reference in its
|
|
68
|
+
* internal injectedHook. Must be true for React ≥ 16.4 to recognise the
|
|
69
|
+
* hook and call onCommitFiberRoot.
|
|
70
|
+
*/
|
|
71
|
+
supportsFiber?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* React calls inject(internals) and stores the returned renderer ID.
|
|
74
|
+
* A minimal no-op implementation (return 1) is sufficient for our purposes.
|
|
75
|
+
*/
|
|
76
|
+
inject?: (internals: unknown) => number;
|
|
77
|
+
[key: string]: unknown;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type CommitListener = (root: {
|
|
81
|
+
current: MinimalFiber;
|
|
82
|
+
}, rendererId: number) => void;
|
|
83
|
+
declare function installDevToolsHook(listener: CommitListener): () => void;
|
|
84
|
+
/**
|
|
85
|
+
* Clears all listeners and forgets our installed hook reference. Used by
|
|
86
|
+
* tests to fully reset module state. Does NOT remove the hook from
|
|
87
|
+
* globalThis — callers that want a fully clean slate must also
|
|
88
|
+
* `delete globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__`.
|
|
89
|
+
*/
|
|
90
|
+
declare function uninstallDevToolsHook(): void;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Return the component name for a fiber.
|
|
94
|
+
*
|
|
95
|
+
* Host components (DOM tags) return their tag string. Function and class
|
|
96
|
+
* components return their displayName or name. Memo and forwardRef wrappers
|
|
97
|
+
* are unwrapped to their inner component. Unknown shapes return null.
|
|
98
|
+
*/
|
|
99
|
+
declare function fiberComponentName(fiber: MinimalFiber | null): string | null;
|
|
100
|
+
interface WalkOptions {
|
|
101
|
+
/**
|
|
102
|
+
* Maximum number of fibers to visit. Prevents runaway traversal on very
|
|
103
|
+
* deep trees. Default 10_000.
|
|
104
|
+
*/
|
|
105
|
+
stopAt?: number;
|
|
106
|
+
/**
|
|
107
|
+
* Predicate deciding whether to descend into a fiber's children. Returning
|
|
108
|
+
* false prunes the entire subtree (the fiber itself is still visited). Used
|
|
109
|
+
* to skip subtrees that did no work this commit. Defaults to always descend.
|
|
110
|
+
*/
|
|
111
|
+
descend?: (fiber: MinimalFiber) => boolean;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Walk a fiber subtree depth-first, invoking `visit` for every fiber with its
|
|
115
|
+
* depth relative to `root` (root is depth 0). Returns early once `stopAt`
|
|
116
|
+
* fibers have been visited. When `descend` returns false for a fiber, its
|
|
117
|
+
* children are skipped entirely.
|
|
118
|
+
*/
|
|
119
|
+
declare function walkChangedFibers(root: MinimalFiber, visit: (fiber: MinimalFiber, depth: number) => void, opts?: WalkOptions): void;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Walk up from the fiber attached to `el` until we find one whose `type` is
|
|
123
|
+
* a function or class component (not a host tag string). Returns that
|
|
124
|
+
* component's display name. If no component is found, returns the host
|
|
125
|
+
* tag name of the starting fiber. If no fiber is attached, returns null.
|
|
126
|
+
*/
|
|
127
|
+
declare function resolveComponentFromElement(el: HTMLElement): string | null;
|
|
128
|
+
|
|
129
|
+
declare function createRenderCollector(): Collector;
|
|
130
|
+
|
|
131
|
+
export { type MinimalFiber, type ReactAdapter, type ReactDevToolsHook, createRenderCollector, fiberComponentName, installDevToolsHook, resolveComponentFromElement, uninstallDevToolsHook, walkChangedFibers };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// src/devtools-hook.ts
|
|
2
|
+
var HOOK_KEY = "__REACT_DEVTOOLS_GLOBAL_HOOK__";
|
|
3
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
4
|
+
var ourHook = null;
|
|
5
|
+
var chainedOriginal = null;
|
|
6
|
+
function ensureHookInstalled() {
|
|
7
|
+
const g = globalThis;
|
|
8
|
+
if (ourHook && g[HOOK_KEY] === ourHook) return;
|
|
9
|
+
const existing = g[HOOK_KEY];
|
|
10
|
+
chainedOriginal = existing && existing !== ourHook && typeof existing.onCommitFiberRoot === "function" ? existing.onCommitFiberRoot : null;
|
|
11
|
+
const hook = existing && existing !== ourHook ? existing : {};
|
|
12
|
+
if (!hook.supportsFiber) {
|
|
13
|
+
hook.supportsFiber = true;
|
|
14
|
+
}
|
|
15
|
+
if (typeof hook.inject !== "function") {
|
|
16
|
+
let _nextRendererId = 1;
|
|
17
|
+
hook.inject = () => _nextRendererId++;
|
|
18
|
+
}
|
|
19
|
+
if (!hook.renderers) {
|
|
20
|
+
;
|
|
21
|
+
hook.renderers = /* @__PURE__ */ new Map();
|
|
22
|
+
}
|
|
23
|
+
if (typeof hook["on"] !== "function") {
|
|
24
|
+
;
|
|
25
|
+
hook["on"] = () => {
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (typeof hook["off"] !== "function") {
|
|
29
|
+
;
|
|
30
|
+
hook["off"] = () => {
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (typeof hook["emit"] !== "function") {
|
|
34
|
+
;
|
|
35
|
+
hook["emit"] = () => {
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (typeof hook["sub"] !== "function") {
|
|
39
|
+
;
|
|
40
|
+
hook["sub"] = () => () => {
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (typeof hook["checkDCE"] !== "function") {
|
|
44
|
+
;
|
|
45
|
+
hook["checkDCE"] = () => {
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (typeof hook["onCommitFiberUnmount"] !== "function") {
|
|
49
|
+
;
|
|
50
|
+
hook["onCommitFiberUnmount"] = () => {
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (typeof hook["onPostCommitFiberRoot"] !== "function") {
|
|
54
|
+
;
|
|
55
|
+
hook["onPostCommitFiberRoot"] = () => {
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
hook.onCommitFiberRoot = (rendererId, root, priorityLevel) => {
|
|
59
|
+
if (chainedOriginal) {
|
|
60
|
+
try {
|
|
61
|
+
chainedOriginal(rendererId, root, priorityLevel);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.warn("[react-perfscope] chained DevTools hook threw:", err);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const cb of listeners) {
|
|
67
|
+
try {
|
|
68
|
+
cb(root, rendererId);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.warn("[react-perfscope] commit listener threw:", err);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
g[HOOK_KEY] = hook;
|
|
75
|
+
ourHook = hook;
|
|
76
|
+
}
|
|
77
|
+
function installDevToolsHook(listener) {
|
|
78
|
+
ensureHookInstalled();
|
|
79
|
+
listeners.add(listener);
|
|
80
|
+
return () => {
|
|
81
|
+
listeners.delete(listener);
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function uninstallDevToolsHook() {
|
|
85
|
+
listeners.clear();
|
|
86
|
+
ourHook = null;
|
|
87
|
+
chainedOriginal = null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/fiber-walker.ts
|
|
91
|
+
var MEMO_TYPE = /* @__PURE__ */ Symbol.for("react.memo");
|
|
92
|
+
var FORWARD_REF_TYPE = /* @__PURE__ */ Symbol.for("react.forward_ref");
|
|
93
|
+
function namedFunctionName(value) {
|
|
94
|
+
if (typeof value !== "function") return null;
|
|
95
|
+
const fn = value;
|
|
96
|
+
if (typeof fn.displayName === "string" && fn.displayName.length > 0) return fn.displayName;
|
|
97
|
+
if (typeof fn.name === "string" && fn.name.length > 0) return fn.name;
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
function fiberComponentName(fiber) {
|
|
101
|
+
if (!fiber) return null;
|
|
102
|
+
const type = fiber.type;
|
|
103
|
+
if (typeof type === "string") return type;
|
|
104
|
+
if (typeof type === "function") return namedFunctionName(type);
|
|
105
|
+
if (type && typeof type === "object") {
|
|
106
|
+
const wrapper = type;
|
|
107
|
+
if (wrapper.$$typeof === MEMO_TYPE) {
|
|
108
|
+
return namedFunctionName(wrapper.type);
|
|
109
|
+
}
|
|
110
|
+
if (wrapper.$$typeof === FORWARD_REF_TYPE) {
|
|
111
|
+
return namedFunctionName(wrapper.render);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
function walkChangedFibers(root, visit, opts = {}) {
|
|
117
|
+
const max = opts.stopAt ?? 1e4;
|
|
118
|
+
const descend = opts.descend ?? (() => true);
|
|
119
|
+
let count = 0;
|
|
120
|
+
let depth = 0;
|
|
121
|
+
let node = root;
|
|
122
|
+
while (node) {
|
|
123
|
+
visit(node, depth);
|
|
124
|
+
count++;
|
|
125
|
+
if (count >= max) return;
|
|
126
|
+
if (node.child && descend(node)) {
|
|
127
|
+
node = node.child;
|
|
128
|
+
depth++;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
while (node && !node.sibling) {
|
|
132
|
+
if (node === root) return;
|
|
133
|
+
node = node.return;
|
|
134
|
+
depth--;
|
|
135
|
+
}
|
|
136
|
+
if (!node || node === root) return;
|
|
137
|
+
node = node.sibling;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/attribution.ts
|
|
142
|
+
var FIBER_KEY_PATTERNS = ["__reactFiber$", "__reactInternalInstance$"];
|
|
143
|
+
function findFiberOnElement(el) {
|
|
144
|
+
for (const key of Object.keys(el)) {
|
|
145
|
+
for (const pattern of FIBER_KEY_PATTERNS) {
|
|
146
|
+
if (key.startsWith(pattern)) {
|
|
147
|
+
return el[key] ?? null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
function resolveComponentFromElement(el) {
|
|
154
|
+
const start = findFiberOnElement(el);
|
|
155
|
+
if (!start) return null;
|
|
156
|
+
let node = start;
|
|
157
|
+
while (node) {
|
|
158
|
+
if (typeof node.type === "function" || node.type && typeof node.type === "object") {
|
|
159
|
+
const name = fiberComponentName(node);
|
|
160
|
+
if (name) return name;
|
|
161
|
+
}
|
|
162
|
+
node = node.return;
|
|
163
|
+
}
|
|
164
|
+
return fiberComponentName(start);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/render-reason.ts
|
|
168
|
+
var PERFORMED_WORK = 1;
|
|
169
|
+
function didPerformWork(fiber) {
|
|
170
|
+
if (!fiber || typeof fiber.flags !== "number") return false;
|
|
171
|
+
return (fiber.flags & PERFORMED_WORK) !== 0;
|
|
172
|
+
}
|
|
173
|
+
function subtreeMightHaveRendered(fiber) {
|
|
174
|
+
if (typeof fiber.subtreeFlags !== "number") return true;
|
|
175
|
+
return (fiber.subtreeFlags & PERFORMED_WORK) !== 0;
|
|
176
|
+
}
|
|
177
|
+
function nearestComponentAncestor(fiber) {
|
|
178
|
+
let p = fiber.return;
|
|
179
|
+
while (p) {
|
|
180
|
+
if (typeof p.type === "function") return p;
|
|
181
|
+
p = p.return;
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
function asObject(value) {
|
|
186
|
+
return value && typeof value === "object" ? value : {};
|
|
187
|
+
}
|
|
188
|
+
function changedPropKeys(prev, next) {
|
|
189
|
+
if (prev === next) return [];
|
|
190
|
+
const p = asObject(prev);
|
|
191
|
+
const n = asObject(next);
|
|
192
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(p), ...Object.keys(n)]);
|
|
193
|
+
const changed = [];
|
|
194
|
+
for (const k of keys) {
|
|
195
|
+
if (p[k] !== n[k]) changed.push(k);
|
|
196
|
+
}
|
|
197
|
+
return changed;
|
|
198
|
+
}
|
|
199
|
+
function classifyRenderReason(fiber) {
|
|
200
|
+
if (!fiber.alternate) return { reason: "mount" };
|
|
201
|
+
const changed = changedPropKeys(fiber.alternate.memoizedProps, fiber.memoizedProps);
|
|
202
|
+
if (changed.length > 0) return { reason: "props", changedProps: changed };
|
|
203
|
+
if (didPerformWork(nearestComponentAncestor(fiber))) return { reason: "parent" };
|
|
204
|
+
return { reason: "state" };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/render-collector.ts
|
|
208
|
+
function createRenderCollector() {
|
|
209
|
+
let active = false;
|
|
210
|
+
let emit = () => {
|
|
211
|
+
};
|
|
212
|
+
let unsubscribe = null;
|
|
213
|
+
let commitId = 0;
|
|
214
|
+
function onCommit(root) {
|
|
215
|
+
if (!active) return;
|
|
216
|
+
const at = performance.now();
|
|
217
|
+
const id = commitId++;
|
|
218
|
+
walkChangedFibers(
|
|
219
|
+
root.current,
|
|
220
|
+
(fiber, depth) => {
|
|
221
|
+
if (typeof fiber.type === "string") return;
|
|
222
|
+
if (!didPerformWork(fiber)) return;
|
|
223
|
+
const name = fiberComponentName(fiber);
|
|
224
|
+
if (!name) return;
|
|
225
|
+
const duration = typeof fiber.actualDuration === "number" ? fiber.actualDuration : 0;
|
|
226
|
+
const { reason, changedProps } = classifyRenderReason(fiber);
|
|
227
|
+
emit({
|
|
228
|
+
kind: "render",
|
|
229
|
+
at,
|
|
230
|
+
component: name,
|
|
231
|
+
reason,
|
|
232
|
+
duration,
|
|
233
|
+
commitId: id,
|
|
234
|
+
depth,
|
|
235
|
+
...changedProps ? { changedProps } : {}
|
|
236
|
+
});
|
|
237
|
+
},
|
|
238
|
+
// Prune subtrees that did no work this commit. Without this, a leaf that
|
|
239
|
+
// mounted long ago keeps its stale PerformedWork flag and gets reported
|
|
240
|
+
// as a phantom render on every unrelated commit.
|
|
241
|
+
{ descend: subtreeMightHaveRendered }
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
kind: "render",
|
|
246
|
+
activate(emitFn) {
|
|
247
|
+
emit = emitFn;
|
|
248
|
+
active = true;
|
|
249
|
+
if (unsubscribe) return;
|
|
250
|
+
unsubscribe = installDevToolsHook(onCommit);
|
|
251
|
+
},
|
|
252
|
+
deactivate() {
|
|
253
|
+
active = false;
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
export {
|
|
258
|
+
createRenderCollector,
|
|
259
|
+
fiberComponentName,
|
|
260
|
+
installDevToolsHook,
|
|
261
|
+
resolveComponentFromElement,
|
|
262
|
+
uninstallDevToolsHook,
|
|
263
|
+
walkChangedFibers
|
|
264
|
+
};
|
|
265
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/devtools-hook.ts","../src/fiber-walker.ts","../src/attribution.ts","../src/render-reason.ts","../src/render-collector.ts"],"sourcesContent":["import type { MinimalFiber, ReactDevToolsHook } from './types'\n\ntype CommitListener = (root: { current: MinimalFiber }, rendererId: number) => void\n\nconst HOOK_KEY = '__REACT_DEVTOOLS_GLOBAL_HOOK__'\n\ninterface GlobalWithHook {\n [HOOK_KEY]?: ReactDevToolsHook\n}\n\nconst listeners = new Set<CommitListener>()\nlet ourHook: ReactDevToolsHook | null = null\nlet chainedOriginal: ReactDevToolsHook['onCommitFiberRoot'] | null = null\n\nfunction ensureHookInstalled(): void {\n const g = globalThis as GlobalWithHook\n // Already ours and still installed — nothing to do.\n if (ourHook && g[HOOK_KEY] === ourHook) return\n\n const existing = g[HOOK_KEY]\n chainedOriginal =\n existing && existing !== ourHook && typeof existing.onCommitFiberRoot === 'function'\n ? existing.onCommitFiberRoot\n : null\n\n const hook: ReactDevToolsHook = existing && existing !== ourHook ? existing : {}\n // React's injectInternals() checks for supportsFiber and calls inject() to\n // get a renderer ID. Without these, React stores injectedHook = null and\n // never calls onCommitFiberRoot on subsequent commits.\n if (!hook.supportsFiber) {\n hook.supportsFiber = true\n }\n if (typeof hook.inject !== 'function') {\n let _nextRendererId = 1\n hook.inject = () => _nextRendererId++\n }\n // Additional fields that React Refresh (@vitejs/plugin-react) accesses on\n // the hook. Without these it throws \"Cannot read properties of undefined\n // (reading 'forEach')\" at injectIntoGlobalHook. We default each to a safe\n // empty/noop value; if the real React DevTools extension installs later\n // it will overwrite these.\n if (!hook.renderers) {\n ;(hook as Record<string, unknown>).renderers = new Map()\n }\n if (typeof (hook as Record<string, unknown>)['on'] !== 'function') {\n ;(hook as Record<string, unknown>)['on'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['off'] !== 'function') {\n ;(hook as Record<string, unknown>)['off'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['emit'] !== 'function') {\n ;(hook as Record<string, unknown>)['emit'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['sub'] !== 'function') {\n ;(hook as Record<string, unknown>)['sub'] = () => () => {}\n }\n if (typeof (hook as Record<string, unknown>)['checkDCE'] !== 'function') {\n ;(hook as Record<string, unknown>)['checkDCE'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['onCommitFiberUnmount'] !== 'function') {\n ;(hook as Record<string, unknown>)['onCommitFiberUnmount'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['onPostCommitFiberRoot'] !== 'function') {\n ;(hook as Record<string, unknown>)['onPostCommitFiberRoot'] = () => {}\n }\n hook.onCommitFiberRoot = (rendererId, root, priorityLevel) => {\n if (chainedOriginal) {\n try {\n chainedOriginal(rendererId, root, priorityLevel)\n } catch (err) {\n console.warn('[react-perfscope] chained DevTools hook threw:', err)\n }\n }\n for (const cb of listeners) {\n try {\n cb(root, rendererId)\n } catch (err) {\n console.warn('[react-perfscope] commit listener threw:', err)\n }\n }\n }\n g[HOOK_KEY] = hook\n ourHook = hook\n}\n\nexport function installDevToolsHook(listener: CommitListener): () => void {\n ensureHookInstalled()\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\n/**\n * Clears all listeners and forgets our installed hook reference. Used by\n * tests to fully reset module state. Does NOT remove the hook from\n * globalThis — callers that want a fully clean slate must also\n * `delete globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__`.\n */\nexport function uninstallDevToolsHook(): void {\n listeners.clear()\n ourHook = null\n chainedOriginal = null\n}\n","import type { MinimalFiber } from './types'\n\nconst MEMO_TYPE = Symbol.for('react.memo')\nconst FORWARD_REF_TYPE = Symbol.for('react.forward_ref')\n\ntype WrapperType = {\n $$typeof: symbol\n type?: unknown\n render?: unknown\n}\n\nfunction namedFunctionName(value: unknown): string | null {\n if (typeof value !== 'function') return null\n const fn = value as { displayName?: string; name?: string }\n if (typeof fn.displayName === 'string' && fn.displayName.length > 0) return fn.displayName\n if (typeof fn.name === 'string' && fn.name.length > 0) return fn.name\n return null\n}\n\n/**\n * Return the component name for a fiber.\n *\n * Host components (DOM tags) return their tag string. Function and class\n * components return their displayName or name. Memo and forwardRef wrappers\n * are unwrapped to their inner component. Unknown shapes return null.\n */\nexport function fiberComponentName(fiber: MinimalFiber | null): string | null {\n if (!fiber) return null\n const type = fiber.type\n if (typeof type === 'string') return type\n if (typeof type === 'function') return namedFunctionName(type)\n if (type && typeof type === 'object') {\n const wrapper = type as WrapperType\n if (wrapper.$$typeof === MEMO_TYPE) {\n return namedFunctionName(wrapper.type)\n }\n if (wrapper.$$typeof === FORWARD_REF_TYPE) {\n return namedFunctionName(wrapper.render)\n }\n }\n return null\n}\n\ninterface WalkOptions {\n /**\n * Maximum number of fibers to visit. Prevents runaway traversal on very\n * deep trees. Default 10_000.\n */\n stopAt?: number\n /**\n * Predicate deciding whether to descend into a fiber's children. Returning\n * false prunes the entire subtree (the fiber itself is still visited). Used\n * to skip subtrees that did no work this commit. Defaults to always descend.\n */\n descend?: (fiber: MinimalFiber) => boolean\n}\n\n/**\n * Walk a fiber subtree depth-first, invoking `visit` for every fiber with its\n * depth relative to `root` (root is depth 0). Returns early once `stopAt`\n * fibers have been visited. When `descend` returns false for a fiber, its\n * children are skipped entirely.\n */\nexport function walkChangedFibers(\n root: MinimalFiber,\n visit: (fiber: MinimalFiber, depth: number) => void,\n opts: WalkOptions = {}\n): void {\n const max = opts.stopAt ?? 10_000\n const descend = opts.descend ?? (() => true)\n let count = 0\n let depth = 0\n let node: MinimalFiber | null = root\n while (node) {\n visit(node, depth)\n count++\n if (count >= max) return\n if (node.child && descend(node)) {\n node = node.child\n depth++\n continue\n }\n while (node && !node.sibling) {\n if (node === root) return\n node = node.return\n depth--\n }\n if (!node || node === root) return\n node = node.sibling\n }\n}\n","import type { MinimalFiber } from './types'\nimport { fiberComponentName } from './fiber-walker'\n\nconst FIBER_KEY_PATTERNS = ['__reactFiber$', '__reactInternalInstance$']\n\nfunction findFiberOnElement(el: HTMLElement): MinimalFiber | null {\n for (const key of Object.keys(el)) {\n for (const pattern of FIBER_KEY_PATTERNS) {\n if (key.startsWith(pattern)) {\n return (el as HTMLElement & Record<string, MinimalFiber>)[key] ?? null\n }\n }\n }\n return null\n}\n\n/**\n * Walk up from the fiber attached to `el` until we find one whose `type` is\n * a function or class component (not a host tag string). Returns that\n * component's display name. If no component is found, returns the host\n * tag name of the starting fiber. If no fiber is attached, returns null.\n */\nexport function resolveComponentFromElement(el: HTMLElement): string | null {\n const start = findFiberOnElement(el)\n if (!start) return null\n let node: MinimalFiber | null = start\n while (node) {\n if (typeof node.type === 'function' || (node.type && typeof node.type === 'object')) {\n const name = fiberComponentName(node)\n if (name) return name\n }\n node = node.return\n }\n // Nothing but host fibers above — return the host tag name of the starting fiber.\n return fiberComponentName(start)\n}\n","import type { MinimalFiber } from './types'\n\n/** React's `PerformedWork` flag — set on a fiber whose render actually ran. */\nexport const PERFORMED_WORK = 0b1\n\nexport type RenderReason = 'mount' | 'state' | 'props' | 'parent'\n\nexport interface RenderReasonResult {\n reason: RenderReason\n changedProps?: string[]\n}\n\nexport function didPerformWork(fiber: MinimalFiber | null): boolean {\n if (!fiber || typeof fiber.flags !== 'number') return false\n return (fiber.flags & PERFORMED_WORK) !== 0\n}\n\n/**\n * Should the walk descend into this fiber's children when hunting for renders?\n *\n * React bubbles the `PerformedWork` bit of every descendant into a fiber's\n * `subtreeFlags`, and (unlike `flags`) resets it whenever the reconciler\n * re-clones the fiber. So a numeric `subtreeFlags` without the bit reliably\n * means \"no descendant rendered this commit\" — we can stop here and avoid\n * stale leaves whose own `flags` never got cleared.\n *\n * When `subtreeFlags` is absent (React < 18) we cannot prune safely, so we\n * descend and fall back to the full-tree walk.\n */\nexport function subtreeMightHaveRendered(fiber: MinimalFiber): boolean {\n if (typeof fiber.subtreeFlags !== 'number') return true\n return (fiber.subtreeFlags & PERFORMED_WORK) !== 0\n}\n\n/**\n * Walk up `.return` to the nearest *component* fiber, skipping host (DOM)\n * fibers. A component's immediate `.return` is usually a host fiber (the\n * `<div>` it's nested in), not its parent component — so to ask \"did my\n * parent component render?\" we have to climb past the host nodes first.\n */\nexport function nearestComponentAncestor(fiber: MinimalFiber): MinimalFiber | null {\n let p = fiber.return\n while (p) {\n if (typeof p.type === 'function') return p\n p = p.return\n }\n return null\n}\n\nfunction asObject(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' ? (value as Record<string, unknown>) : {}\n}\n\n/** Shallow diff of two prop bags. Returns the keys whose values differ. */\nexport function changedPropKeys(prev: unknown, next: unknown): string[] {\n if (prev === next) return []\n const p = asObject(prev)\n const n = asObject(next)\n const keys = new Set([...Object.keys(p), ...Object.keys(n)])\n const changed: string[] = []\n for (const k of keys) {\n if (p[k] !== n[k]) changed.push(k)\n }\n return changed\n}\n\n/**\n * Classify WHY a fiber that performed work this commit rendered.\n *\n * - `mount` — first render (no previous fiber).\n * - `props` — at least one prop changed since the previous render.\n * - `state` — props are identical AND the parent did not render, so the\n * update originated here (own state/force update). This is the\n * root of a render cascade.\n * - `parent` — props are identical but the parent rendered, so this only\n * re-rendered because of its parent. This is the avoidable\n * \"wrap me in memo\" case the developer is hunting for.\n *\n * Assumes the caller only invokes this for fibers that actually performed\n * work (see `didPerformWork`).\n */\nexport function classifyRenderReason(fiber: MinimalFiber): RenderReasonResult {\n if (!fiber.alternate) return { reason: 'mount' }\n const changed = changedPropKeys(fiber.alternate.memoizedProps, fiber.memoizedProps)\n if (changed.length > 0) return { reason: 'props', changedProps: changed }\n if (didPerformWork(nearestComponentAncestor(fiber))) return { reason: 'parent' }\n return { reason: 'state' }\n}\n","import type { Collector, Signal } from '@react-perfscope/core'\nimport { installDevToolsHook } from './devtools-hook'\nimport { fiberComponentName, walkChangedFibers } from './fiber-walker'\nimport { classifyRenderReason, didPerformWork, subtreeMightHaveRendered } from './render-reason'\nimport type { MinimalFiber } from './types'\n\nexport function createRenderCollector(): Collector {\n let active = false\n let emit: (signal: Signal) => void = () => {}\n let unsubscribe: (() => void) | null = null\n let commitId = 0\n\n function onCommit(root: { current: MinimalFiber }) {\n if (!active) return\n const at = performance.now()\n const id = commitId++\n walkChangedFibers(\n root.current,\n (fiber, depth) => {\n // Skip host (DOM) fibers — we only want function/class components in\n // render reports.\n if (typeof fiber.type === 'string') return\n // Only report fibers that actually re-ran their render this commit.\n // A bailed-out fiber (e.g. memo with equal props) keeps its place in\n // the tree but did no work, so reporting it would be noise.\n if (!didPerformWork(fiber)) return\n const name = fiberComponentName(fiber)\n if (!name) return\n const duration = typeof fiber.actualDuration === 'number' ? fiber.actualDuration : 0\n const { reason, changedProps } = classifyRenderReason(fiber)\n emit({\n kind: 'render',\n at,\n component: name,\n reason,\n duration,\n commitId: id,\n depth,\n ...(changedProps ? { changedProps } : {}),\n })\n },\n // Prune subtrees that did no work this commit. Without this, a leaf that\n // mounted long ago keeps its stale PerformedWork flag and gets reported\n // as a phantom render on every unrelated commit.\n { descend: subtreeMightHaveRendered }\n )\n }\n\n return {\n kind: 'render',\n activate(emitFn) {\n emit = emitFn\n active = true\n if (unsubscribe) return\n unsubscribe = installDevToolsHook(onCommit)\n },\n deactivate() {\n active = false\n // Keep the global hook listener attached — installDevToolsHook is\n // idempotent and removing it on every deactivate would cost us the\n // ability to re-attach cleanly under the test reset hooks. The\n // `active` flag gates emission.\n },\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@react-perfscope/react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React render-cascade collector for react-perfscope: per-commit render reasons with source attribution.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react",
|
|
7
|
+
"performance",
|
|
8
|
+
"profiler",
|
|
9
|
+
"render",
|
|
10
|
+
"re-render",
|
|
11
|
+
"devtools"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "./dist/index.cjs",
|
|
15
|
+
"module": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js",
|
|
21
|
+
"require": "./dist/index.cjs"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@react-perfscope/core": "0.1.0"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"react": "^18.3.0",
|
|
32
|
+
"react-dom": "^18.3.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"react": "^18.3.0",
|
|
36
|
+
"react-dom": "^18.3.0",
|
|
37
|
+
"@types/react": "^18.3.0",
|
|
38
|
+
"@types/react-dom": "^18.3.0",
|
|
39
|
+
"@testing-library/react": "^16.0.0"
|
|
40
|
+
},
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"author": "rayforvideos",
|
|
43
|
+
"homepage": "https://github.com/rayforvideos/react-perfscope#readme",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/rayforvideos/react-perfscope.git",
|
|
47
|
+
"directory": "packages/react"
|
|
48
|
+
},
|
|
49
|
+
"bugs": "https://github.com/rayforvideos/react-perfscope/issues",
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsup && node scripts/prepend-dts-refs.mjs",
|
|
55
|
+
"typecheck": "tsc --noEmit"
|
|
56
|
+
}
|
|
57
|
+
}
|