@traceletdev/react 0.5.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/README.md +67 -0
- package/index.js +188 -0
- package/package.json +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# @traceletdev/react
|
|
2
|
+
|
|
3
|
+
Tracks React component **re-renders** and surfaces them in the Tracelet HUD's Components tab.
|
|
4
|
+
|
|
5
|
+
It works by installing the React DevTools global hook (`__REACT_DEVTOOLS_GLOBAL_HOOK__`) and
|
|
6
|
+
counting each commit via `onCommitFiberRoot` — the same mechanism React DevTools uses. Components
|
|
7
|
+
that bail out (e.g. wrapped in `React.memo` with unchanged props) are correctly **not** counted.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -D @traceletdev/react
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage — must load before React
|
|
16
|
+
|
|
17
|
+
> **Important:** the hook must be installed **before React initializes**, because React reads
|
|
18
|
+
> `__REACT_DEVTOOLS_GLOBAL_HOOK__` only once, at startup. Loading it afterwards tracks nothing.
|
|
19
|
+
|
|
20
|
+
### Bundled app (Next.js, Vite, CRA, …)
|
|
21
|
+
|
|
22
|
+
Import it as the **first line** of your dev entry point:
|
|
23
|
+
|
|
24
|
+
```javascript
|
|
25
|
+
// main.jsx / index.tsx — top of file, before any React import
|
|
26
|
+
if (import.meta.env.DEV) {
|
|
27
|
+
await import('@traceletdev/react');
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
or with CommonJS:
|
|
32
|
+
|
|
33
|
+
```javascript
|
|
34
|
+
if (process.env.NODE_ENV === 'development') {
|
|
35
|
+
require('@traceletdev/react');
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Plain HTML / when running the HUD
|
|
40
|
+
|
|
41
|
+
Add the hook served by the HUD to `<head>`, **before** your app bundle:
|
|
42
|
+
|
|
43
|
+
```html
|
|
44
|
+
<head>
|
|
45
|
+
<script src="http://localhost:3111/hook.js"></script>
|
|
46
|
+
</head>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The HUD's `overlay.js` also injects the instrumentation as a best-effort fallback, but that runs
|
|
50
|
+
after your app loads — it only works if the React DevTools extension is already present. For
|
|
51
|
+
reliable tracking, use one of the two methods above.
|
|
52
|
+
|
|
53
|
+
## API
|
|
54
|
+
|
|
55
|
+
Exposes `window.__traceletReactInstrumentation`:
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
window.__traceletReactInstrumentation.getComponents();
|
|
59
|
+
// [{ name, renderCount, instances, maxRenders }, ...]
|
|
60
|
+
// renderCount is the total across all instances sharing a name; maxRenders is
|
|
61
|
+
// the hottest single instance — the number worth investigating.
|
|
62
|
+
|
|
63
|
+
window.__traceletReactInstrumentation.getRenderCounts(); // { ComponentName: totalCount, ... }
|
|
64
|
+
window.__traceletReactInstrumentation.reset(); // zero counts (keeps mount/instance identity)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Dev only — do not ship to production.
|
package/index.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tracelet React Instrumentation
|
|
5
|
+
*
|
|
6
|
+
* Counts component re-renders by installing the React DevTools global hook
|
|
7
|
+
* (window.__REACT_DEVTOOLS_GLOBAL_HOOK__) and inspecting each commit via
|
|
8
|
+
* onCommitFiberRoot. This is the only reliable way to observe renders in a
|
|
9
|
+
* real (bundled / JSX-runtime) app — patching React.createElement does not
|
|
10
|
+
* work because compiled JSX never calls the global React.createElement.
|
|
11
|
+
*
|
|
12
|
+
* HARD REQUIREMENT: this must run BEFORE React initializes, because React
|
|
13
|
+
* only reads __REACT_DEVTOOLS_GLOBAL_HOOK__ once, at module init. Load it via
|
|
14
|
+
* <script src=".../hook.js"> in <head> before your bundle, or as the first
|
|
15
|
+
* import in your app entry (dev only).
|
|
16
|
+
*
|
|
17
|
+
* ponytail: canonical source. Kept byte-identical with
|
|
18
|
+
* packages/tracelet-react/index.js — edit both together.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
(function () {
|
|
22
|
+
if (typeof window === 'undefined') return;
|
|
23
|
+
if (window.__traceletReactInstrumentation) return;
|
|
24
|
+
|
|
25
|
+
var DEBUG = false; // set window.__TRACELET_DEBUG = true to trace
|
|
26
|
+
function log() {
|
|
27
|
+
if (window.__TRACELET_DEBUG || DEBUG) {
|
|
28
|
+
try { console.log.apply(console, ['[tracelet]'].concat([].slice.call(arguments))); } catch (e) {}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
var idMap = new WeakMap(); // fiber -> instance id (fibers are non-extensible,
|
|
33
|
+
// so we can't tag them directly — WeakMap works)
|
|
34
|
+
var instances = {}; // instance id -> { name, count }
|
|
35
|
+
var nextId = 1; // monotonic; never reset (avoids id reuse collisions)
|
|
36
|
+
|
|
37
|
+
// Stable per-instance id across the fiber double-buffer: current/alternate
|
|
38
|
+
// swap each commit, so both halves map to the same id.
|
|
39
|
+
function instanceId(fiber) {
|
|
40
|
+
var id = idMap.get(fiber) || (fiber.alternate && idMap.get(fiber.alternate));
|
|
41
|
+
if (!id) id = nextId++;
|
|
42
|
+
idMap.set(fiber, id);
|
|
43
|
+
if (fiber.alternate) idMap.set(fiber.alternate, id);
|
|
44
|
+
return id;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Drop unhelpful names. React components are PascalCase by convention (a
|
|
48
|
+
// lowercase tag is a host element), so anything not starting uppercase is
|
|
49
|
+
// either anonymous or a minified library internal ("oi", "rU", "u7"). Names
|
|
50
|
+
// with a dot (framer-motion's motion.div / motion.create()) are kept.
|
|
51
|
+
function isNoise(name) {
|
|
52
|
+
if (!name || name === 'Anonymous' || name === 'Unknown') return true;
|
|
53
|
+
return name.indexOf('.') === -1 && !/^[A-Z]/.test(name);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getDisplayName(fiber) {
|
|
57
|
+
var type = fiber.type;
|
|
58
|
+
var name = null;
|
|
59
|
+
if (typeof type === 'function') {
|
|
60
|
+
name = type.displayName || type.name || null;
|
|
61
|
+
} else if (type && typeof type === 'object') {
|
|
62
|
+
// React.memo (type.type) or forwardRef (type.render)
|
|
63
|
+
var inner = type.type || type.render;
|
|
64
|
+
name = type.displayName ||
|
|
65
|
+
(typeof inner === 'function' ? (inner.displayName || inner.name) : null) || null;
|
|
66
|
+
}
|
|
67
|
+
return isNoise(name) ? null : name;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// We walk the whole current tree each commit, so we must distinguish fibers
|
|
71
|
+
// that rendered THIS commit from ones just sitting in a bailed-out subtree.
|
|
72
|
+
// - Updated fibers have an alternate; they rendered iff memoizedProps or
|
|
73
|
+
// memoizedState changed vs it (a bailout reuses both references). The
|
|
74
|
+
// PerformedWork flag is unreliable — it stays stale on reused fibers.
|
|
75
|
+
// - Never-updated fibers keep alternate === null forever, so we count their
|
|
76
|
+
// mount exactly once (rec.seen) rather than on every walk.
|
|
77
|
+
function onCommit(root) {
|
|
78
|
+
if (!root || !root.current) return;
|
|
79
|
+
var stack = [root.current];
|
|
80
|
+
var guard = 0;
|
|
81
|
+
while (stack.length) {
|
|
82
|
+
var fiber = stack.pop();
|
|
83
|
+
if (!fiber) continue;
|
|
84
|
+
var name = getDisplayName(fiber);
|
|
85
|
+
if (name) {
|
|
86
|
+
var id = instanceId(fiber);
|
|
87
|
+
var rec = instances[id] || (instances[id] = { name: name, count: 0, seen: false });
|
|
88
|
+
rec.name = name;
|
|
89
|
+
var alt = fiber.alternate;
|
|
90
|
+
var rendered = alt == null
|
|
91
|
+
? !rec.seen // mount: count once
|
|
92
|
+
: (alt.memoizedProps !== fiber.memoizedProps || alt.memoizedState !== fiber.memoizedState);
|
|
93
|
+
if (rendered) rec.count++;
|
|
94
|
+
rec.seen = true;
|
|
95
|
+
}
|
|
96
|
+
if (fiber.child) stack.push(fiber.child);
|
|
97
|
+
if (fiber.sibling) stack.push(fiber.sibling);
|
|
98
|
+
if (++guard > 200000) break; // safety against cycles
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function safeCommit(root) {
|
|
103
|
+
try { onCommit(root); } catch (e) { log('commit error', e); }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Drop an instance's record when its fiber unmounts, so stale components don't
|
|
107
|
+
// linger in the list.
|
|
108
|
+
function onUnmount(fiber) {
|
|
109
|
+
var id = idMap.get(fiber) || (fiber.alternate && idMap.get(fiber.alternate));
|
|
110
|
+
if (id && instances[id]) delete instances[id];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function safeUnmount(fiber) {
|
|
114
|
+
try { onUnmount(fiber); } catch (e) { log('unmount error', e); }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Install (or wrap) the DevTools global hook.
|
|
118
|
+
var existing = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
119
|
+
if (existing) {
|
|
120
|
+
// Real DevTools (or another tool) already present — chain onto it so we
|
|
121
|
+
// don't break it. We only see commits from this point forward.
|
|
122
|
+
var origOnCommit = existing.onCommitFiberRoot;
|
|
123
|
+
existing.onCommitFiberRoot = function (id, root) {
|
|
124
|
+
safeCommit(root);
|
|
125
|
+
if (typeof origOnCommit === 'function') return origOnCommit.apply(this, arguments);
|
|
126
|
+
};
|
|
127
|
+
var origUnmount = existing.onCommitFiberUnmount;
|
|
128
|
+
existing.onCommitFiberUnmount = function (id, fiber) {
|
|
129
|
+
safeUnmount(fiber);
|
|
130
|
+
if (typeof origUnmount === 'function') return origUnmount.apply(this, arguments);
|
|
131
|
+
};
|
|
132
|
+
log('wrapped existing DevTools hook');
|
|
133
|
+
} else {
|
|
134
|
+
var renderers = new Map();
|
|
135
|
+
var uid = 0;
|
|
136
|
+
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
|
|
137
|
+
renderers: renderers,
|
|
138
|
+
supportsFiber: true,
|
|
139
|
+
inject: function (renderer) { var id = ++uid; renderers.set(id, renderer); return id; },
|
|
140
|
+
onCommitFiberRoot: function (id, root) { safeCommit(root); },
|
|
141
|
+
onCommitFiberUnmount: function (id, fiber) { safeUnmount(fiber); },
|
|
142
|
+
on: function () {},
|
|
143
|
+
emit: function () {},
|
|
144
|
+
checkDCE: function () {}
|
|
145
|
+
};
|
|
146
|
+
log('installed minimal DevTools hook');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Aggregate per-instance data by display name, keeping the instance count and
|
|
150
|
+
// the hottest single instance — so 1 instance rendering 200x (a real smell) is
|
|
151
|
+
// distinguishable from 40 instances rendering 5x each.
|
|
152
|
+
function aggregate() {
|
|
153
|
+
var byName = {};
|
|
154
|
+
for (var id in instances) {
|
|
155
|
+
var r = instances[id];
|
|
156
|
+
if (r.count === 0) continue; // skip instances that haven't rendered (e.g. post-reset)
|
|
157
|
+
var g = byName[r.name] || (byName[r.name] = { name: r.name, instances: 0, renderCount: 0, maxRenders: 0 });
|
|
158
|
+
g.instances++;
|
|
159
|
+
g.renderCount += r.count;
|
|
160
|
+
if (r.count > g.maxRenders) g.maxRenders = r.count;
|
|
161
|
+
}
|
|
162
|
+
return byName;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
window.__traceletReactInstrumentation = {
|
|
166
|
+
getComponents: function () {
|
|
167
|
+
var byName = aggregate();
|
|
168
|
+
var out = [];
|
|
169
|
+
for (var n in byName) out.push(byName[n]);
|
|
170
|
+
return out;
|
|
171
|
+
},
|
|
172
|
+
getRenderCounts: function () {
|
|
173
|
+
var byName = aggregate();
|
|
174
|
+
var counts = {};
|
|
175
|
+
for (var n in byName) counts[n] = byName[n].renderCount;
|
|
176
|
+
return counts;
|
|
177
|
+
},
|
|
178
|
+
// Zero counts but keep instance records (and their `seen` flags) so already
|
|
179
|
+
// mounted components aren't re-counted as fresh mounts — after reset, only
|
|
180
|
+
// components that actually re-render show up.
|
|
181
|
+
reset: function () { for (var id in instances) instances[id].count = 0; }
|
|
182
|
+
};
|
|
183
|
+
})();
|
|
184
|
+
|
|
185
|
+
// Export for module systems (harmless/no-op in the browser <script> case).
|
|
186
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
187
|
+
module.exports = (typeof window !== 'undefined') ? window.__traceletReactInstrumentation : {};
|
|
188
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@traceletdev/react",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Runtime instrumentation for React component render tracking",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.js",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"react",
|
|
15
|
+
"performance",
|
|
16
|
+
"tracelet",
|
|
17
|
+
"rerender",
|
|
18
|
+
"profiling"
|
|
19
|
+
],
|
|
20
|
+
"author": "Moiz Imran <moizwasti@gmail.com>",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"react": ">=16.0.0"
|
|
24
|
+
}
|
|
25
|
+
}
|