@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.
Files changed (3) hide show
  1. package/README.md +67 -0
  2. package/index.js +188 -0
  3. 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
+ }