react-debug-updates 0.1.7 → 0.1.10
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 +40 -50
- package/dist/format.d.ts +2 -2
- package/dist/highlights.d.ts +5 -0
- package/dist/index.d.ts +2 -2
- package/dist/monitor.d.ts +15 -0
- package/dist/react-debug-updates.cjs +56 -57
- package/dist/react-debug-updates.cjs.map +1 -1
- package/dist/react-debug-updates.js +56 -57
- package/dist/react-debug-updates.js.map +1 -1
- package/dist/types.d.ts +20 -33
- package/package.json +1 -1
- package/dist/batcher.d.ts +0 -6
- package/dist/logger.d.ts +0 -11
package/README.md
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
# react-debug-updates
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
See exactly which React components re-render, how often, how long they take, and *why* — all without modifying your components.
|
|
4
4
|
|
|
5
5
|
 
|
|
6
6
|
|
|
7
7
|
<img src="demo.gif" alt="demo" width="852" height="476" />
|
|
8
8
|
|
|
9
|
+
## Why?
|
|
10
|
+
|
|
11
|
+
I was working on an Electron app and spent hours trying to get the official React DevTools to work with it. DevTools' Electron integration is fragile, poorly documented, and breaks between versions. I just needed to see which components were re-rendering so I could fix performance issues.
|
|
12
|
+
|
|
13
|
+
So I wrote this — a plug-and-play one-liner that gives you visual highlight overlays and console logging for React re-renders. No browser extension, no Electron hacks, no configuration. Works in any React web environment — browsers, Electron, iframes.
|
|
14
|
+
|
|
9
15
|
## How it works
|
|
10
16
|
|
|
11
|
-
Hooks into `__REACT_DEVTOOLS_GLOBAL_HOOK__` to intercept every React commit.
|
|
17
|
+
Hooks into `__REACT_DEVTOOLS_GLOBAL_HOOK__` to intercept every React commit. Uses the same fiber tree diffing approach as React DevTools to detect which components actually re-rendered. No React DevTools extension required. No wrappers, no HOCs, no code changes — just call `startReactUpdatesMonitor()` and you get:
|
|
12
18
|
|
|
19
|
+
- **Visual highlights** — highlight boxes on re-rendered DOM nodes with a heat-map color scale (blue → red as render count increases)
|
|
13
20
|
- **Console logging** — grouped, color-coded re-render reports with component tree paths and render durations
|
|
14
|
-
- **
|
|
15
|
-
- **Cause detection** — pinpoint *which* `useState`, `useReducer`, `useSyncExternalStore`, or `useContext` hook triggered each re-render, with previous→next values
|
|
21
|
+
- **Update reasons** — pinpoint *which* `useState`, `useReducer`, `useSyncExternalStore`, or `useContext` hook triggered each re-render, with previous→next values
|
|
16
22
|
|
|
17
23
|
## Install
|
|
18
24
|
|
|
@@ -26,77 +32,61 @@ pnpm add react-debug-updates
|
|
|
26
32
|
|
|
27
33
|
## Quick start
|
|
28
34
|
|
|
29
|
-
Import and call `
|
|
35
|
+
Import and call `startReactUpdatesMonitor` **before** React renders anything — ideally at the very top of your entry point. This ensures the hook is in place before the first commit.
|
|
30
36
|
|
|
31
37
|
```ts
|
|
32
|
-
import {
|
|
38
|
+
import { startReactUpdatesMonitor } from "react-debug-updates";
|
|
33
39
|
|
|
34
|
-
//
|
|
35
|
-
const
|
|
36
|
-
highlight: true,
|
|
37
|
-
showCauses: true,
|
|
38
|
-
});
|
|
40
|
+
// One-liner — visual highlights out of the box
|
|
41
|
+
const stop = startReactUpdatesMonitor();
|
|
39
42
|
|
|
40
43
|
// Later, to clean up:
|
|
41
|
-
|
|
44
|
+
stop?.();
|
|
42
45
|
```
|
|
43
46
|
|
|
44
47
|
### Dev-only guard
|
|
45
48
|
|
|
46
49
|
```ts
|
|
47
50
|
if (process.env.NODE_ENV === "development") {
|
|
48
|
-
const {
|
|
49
|
-
|
|
51
|
+
const { startReactUpdatesMonitor } = await import("react-debug-updates");
|
|
52
|
+
startReactUpdatesMonitor();
|
|
50
53
|
}
|
|
51
54
|
```
|
|
52
55
|
|
|
56
|
+
### With options
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
startReactUpdatesMonitor({
|
|
60
|
+
reasonOfUpdate: true,
|
|
61
|
+
logToConsole: true,
|
|
62
|
+
highlightOpacity: 0.5,
|
|
63
|
+
highlightShowLabels: false,
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
53
67
|
## Requirements
|
|
54
68
|
|
|
55
69
|
- A **React dev build** (which automatically creates `__REACT_DEVTOOLS_GLOBAL_HOOK__`) — no browser extension needed
|
|
56
|
-
- For `
|
|
70
|
+
- For `reasonOfUpdate` and render durations: React must be in **dev mode** (provides `_debugHookTypes` and `actualDuration` on fibers)
|
|
57
71
|
|
|
58
72
|
## API
|
|
59
73
|
|
|
60
|
-
### `
|
|
74
|
+
### `startReactUpdatesMonitor(options?): (() => void) | null`
|
|
61
75
|
|
|
62
|
-
Returns a `
|
|
76
|
+
Returns a `stop` function to unhook from React and remove all overlays, or `null` if the DevTools hook is not available.
|
|
63
77
|
|
|
64
78
|
#### Options
|
|
65
79
|
|
|
66
80
|
| Option | Type | Default | Description |
|
|
67
81
|
| --- | --- | --- | --- |
|
|
68
|
-
| `silent` | `boolean` | `false` | Suppress console output |
|
|
69
82
|
| `mode` | `"self-triggered" \| "all"` | `"self-triggered"` | `"self-triggered"` tracks only components whose own state changed. `"all"` includes children swept by parent updates |
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
72
|
-
| `highlight` | `boolean
|
|
73
|
-
| `
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
| Option | Type | Default | Description |
|
|
78
|
-
| --- | --- | --- | --- |
|
|
79
|
-
| `flushInterval` | `number` | `250` | Milliseconds between overlay flush cycles |
|
|
80
|
-
| `animationDuration` | `number` | `1200` | Overlay fade-out animation duration (ms) |
|
|
81
|
-
| `showLabels` | `boolean` | `true` | Show text labels (name, count, duration, cause) above overlays |
|
|
82
|
-
| `opacity` | `number` | `0.3` | Peak opacity of overlay highlights (0–1) |
|
|
83
|
-
|
|
84
|
-
### `RenderLogger`
|
|
85
|
-
|
|
86
|
-
| Property | Type | Description |
|
|
87
|
-
| --- | --- | --- |
|
|
88
|
-
| `entries` | `RenderEntry[]` | Ring buffer of recorded re-render entries |
|
|
89
|
-
| `disconnect` | `() => void` | Unhook from React and remove all overlays |
|
|
90
|
-
|
|
91
|
-
### `RenderEntry`
|
|
92
|
-
|
|
93
|
-
| Property | Type | Description |
|
|
94
|
-
| --- | --- | --- |
|
|
95
|
-
| `component` | `string` | Component display name |
|
|
96
|
-
| `path` | `string` | Ancestor component path (e.g. `"App → Layout → Sidebar"`) |
|
|
97
|
-
| `duration` | `number` | Render duration in ms (requires React dev mode) |
|
|
98
|
-
| `timestamp` | `number` | `performance.now()` when the entry was recorded |
|
|
99
|
-
| `causes` | `UpdateCause[]` | Why this component re-rendered (requires `showCauses`) |
|
|
83
|
+
| `reasonOfUpdate` | `boolean` | `false` | Detect and display why each component re-rendered |
|
|
84
|
+
| `logToConsole` | `boolean` | `false` | Log re-renders to the console |
|
|
85
|
+
| `highlight` | `boolean` | `true` | Enable visual highlight overlays |
|
|
86
|
+
| `highlightShowLabels` | `boolean` | `true` | Show text labels (name, count, duration, cause) above highlights |
|
|
87
|
+
| `highlightOpacity` | `number` | `0.3` | Peak opacity of highlight overlays (0–1) |
|
|
88
|
+
| `highlightFlushInterval` | `number` | `250` | Milliseconds between highlight flush cycles |
|
|
89
|
+
| `highlightAnimationDuration` | `number` | `1200` | Highlight fade-out animation duration (ms) |
|
|
100
90
|
|
|
101
91
|
### `UpdateCause`
|
|
102
92
|
|
|
@@ -120,11 +110,11 @@ Returns a `RenderLogger` handle, or `null` if the DevTools hook is not available
|
|
|
120
110
|
↳ useContext changed
|
|
121
111
|
```
|
|
122
112
|
|
|
123
|
-
## Visual
|
|
113
|
+
## Visual highlights
|
|
124
114
|
|
|
125
115
|
Re-rendered components get a highlight box that fades out. The color shifts from blue to red as the same node re-renders repeatedly within a flush window — making "hot" components visually obvious.
|
|
126
116
|
|
|
127
|
-
Each
|
|
117
|
+
Each highlight label shows: `ComponentName ×count duration (cause)`
|
|
128
118
|
|
|
129
119
|
## License
|
|
130
120
|
|
package/dist/format.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { HighlightEntry, UpdateCause } from "./types.js";
|
|
2
2
|
export declare function formatValue(value: unknown, maxLength?: number): string;
|
|
3
3
|
/** Compact summary for overlay labels. */
|
|
4
4
|
export declare function formatCausesShort(causes: UpdateCause[]): string;
|
|
5
5
|
/** Detailed lines for console output. */
|
|
6
6
|
export declare function formatCausesConsole(causes: UpdateCause[]): string[];
|
|
7
7
|
/** Log re-renders as a collapsed console group. */
|
|
8
|
-
export declare function logRerendersToConsole(entries:
|
|
8
|
+
export declare function logRerendersToConsole(entries: HighlightEntry[], showCauses: boolean): void;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export type {
|
|
1
|
+
export { startReactUpdatesMonitor } from "./monitor.js";
|
|
2
|
+
export type { MonitorOptions, UpdateCause } from "./types.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { MonitorOptions } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Start monitoring React re-renders.
|
|
4
|
+
*
|
|
5
|
+
* Hooks into `__REACT_DEVTOOLS_GLOBAL_HOOK__.onCommitFiberRoot` to intercept
|
|
6
|
+
* every React commit. Shows visual highlight overlays on re-rendered DOM nodes
|
|
7
|
+
* and optionally logs re-renders to the console.
|
|
8
|
+
*
|
|
9
|
+
* Call this **before** React renders anything — ideally at the very top of
|
|
10
|
+
* your entry point.
|
|
11
|
+
*
|
|
12
|
+
* Returns a `stop` function to unhook and clean up, or `null` if called
|
|
13
|
+
* in a non-browser environment (e.g. SSR).
|
|
14
|
+
*/
|
|
15
|
+
export declare function startReactUpdatesMonitor({ logToConsole, highlight, mode, reasonOfUpdate, highlightFlushInterval, highlightAnimationDuration, highlightShowLabels, highlightOpacity, }?: MonitorOptions): (() => void) | null;
|
|
@@ -322,13 +322,7 @@ function heatColor(count, alpha) {
|
|
|
322
322
|
const lightness = 55 - normalizedCount * 10;
|
|
323
323
|
return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`;
|
|
324
324
|
}
|
|
325
|
-
|
|
326
|
-
flushInterval: 250,
|
|
327
|
-
animationDuration: 1200,
|
|
328
|
-
showLabels: true,
|
|
329
|
-
opacity: 0.3
|
|
330
|
-
};
|
|
331
|
-
function createBatcher(options) {
|
|
325
|
+
function createHighlighter(config) {
|
|
332
326
|
let pending = [];
|
|
333
327
|
let timer = null;
|
|
334
328
|
function flush() {
|
|
@@ -381,10 +375,10 @@ function createBatcher(options) {
|
|
|
381
375
|
style.height = `${rect.height}px`;
|
|
382
376
|
style.backgroundColor = fillColor;
|
|
383
377
|
style.border = `1.5px solid ${borderColor}`;
|
|
384
|
-
style.setProperty("--rdu-opacity", String(
|
|
385
|
-
style.animation = `${OVERLAY_ANIMATION_NAME} ${
|
|
378
|
+
style.setProperty("--rdu-opacity", String(config.opacity));
|
|
379
|
+
style.animation = `${OVERLAY_ANIMATION_NAME} ${config.animationDuration}ms ease-out forwards`;
|
|
386
380
|
const label = element.firstElementChild;
|
|
387
|
-
if (
|
|
381
|
+
if (config.showLabels) {
|
|
388
382
|
const countText = coalesced.count > 1 ? ` ×${coalesced.count}` : "";
|
|
389
383
|
const durationText = coalesced.totalDuration > 0 ? ` ${coalesced.totalDuration.toFixed(1)}ms` : "";
|
|
390
384
|
const causeText = coalesced.causeSummary ? ` (${coalesced.causeSummary})` : "";
|
|
@@ -399,7 +393,7 @@ function createBatcher(options) {
|
|
|
399
393
|
function push(entry) {
|
|
400
394
|
pending.push(entry);
|
|
401
395
|
if (!timer) {
|
|
402
|
-
timer = setInterval(flush,
|
|
396
|
+
timer = setInterval(flush, config.flushInterval);
|
|
403
397
|
}
|
|
404
398
|
}
|
|
405
399
|
function dispose() {
|
|
@@ -411,75 +405,80 @@ function createBatcher(options) {
|
|
|
411
405
|
}
|
|
412
406
|
return { push, dispose };
|
|
413
407
|
}
|
|
414
|
-
function
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
408
|
+
function ensureDevToolsHook(win) {
|
|
409
|
+
const global = win;
|
|
410
|
+
if (!global.__REACT_DEVTOOLS_GLOBAL_HOOK__) {
|
|
411
|
+
global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
|
|
412
|
+
supportsFiber: true,
|
|
413
|
+
inject() {
|
|
414
|
+
return 1;
|
|
415
|
+
},
|
|
416
|
+
onCommitFiberRoot() {
|
|
417
|
+
},
|
|
418
|
+
onCommitFiberUnmount() {
|
|
419
|
+
},
|
|
420
|
+
onPostCommitFiberRoot() {
|
|
421
|
+
},
|
|
422
|
+
checkDCE() {
|
|
423
|
+
}
|
|
424
|
+
};
|
|
429
425
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
426
|
+
return global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
427
|
+
}
|
|
428
|
+
function startReactUpdatesMonitor({
|
|
429
|
+
logToConsole = false,
|
|
430
|
+
highlight = true,
|
|
431
|
+
mode = "self-triggered",
|
|
432
|
+
reasonOfUpdate = false,
|
|
433
|
+
highlightFlushInterval = 250,
|
|
434
|
+
highlightAnimationDuration = 1200,
|
|
435
|
+
highlightShowLabels = true,
|
|
436
|
+
highlightOpacity = 0.3
|
|
437
|
+
} = {}) {
|
|
438
|
+
if (typeof window === "undefined") return null;
|
|
439
|
+
const hook = ensureDevToolsHook(window);
|
|
440
|
+
const highlighter = highlight ? createHighlighter({
|
|
441
|
+
flushInterval: highlightFlushInterval,
|
|
442
|
+
animationDuration: highlightAnimationDuration,
|
|
443
|
+
showLabels: highlightShowLabels,
|
|
444
|
+
opacity: highlightOpacity
|
|
445
|
+
}) : null;
|
|
436
446
|
const previousOnCommit = hook.onCommitFiberRoot.bind(hook);
|
|
437
447
|
hook.onCommitFiberRoot = (rendererID, root, priorityLevel) => {
|
|
438
448
|
previousOnCommit(rendererID, root, priorityLevel);
|
|
439
449
|
const detectedRenders = detectRenders(root.current, mode);
|
|
440
450
|
if (detectedRenders.length === 0) return;
|
|
441
|
-
const
|
|
451
|
+
const highlightEntries = [];
|
|
442
452
|
for (let i = 0; i < detectedRenders.length; i++) {
|
|
443
453
|
const { fiber, depth } = detectedRenders[i];
|
|
444
454
|
const name = getComponentName(fiber);
|
|
445
455
|
if (!name) continue;
|
|
446
|
-
const
|
|
456
|
+
const entry = {
|
|
447
457
|
component: name,
|
|
448
458
|
path: getFiberPath(fiber),
|
|
449
459
|
duration: fiber.actualDuration ?? 0,
|
|
450
460
|
depth,
|
|
451
461
|
domNode: findNearestDOMNode(fiber),
|
|
452
|
-
causes:
|
|
453
|
-
};
|
|
454
|
-
const renderEntry = {
|
|
455
|
-
component: pendingEntry.component,
|
|
456
|
-
path: pendingEntry.path,
|
|
457
|
-
duration: pendingEntry.duration,
|
|
458
|
-
timestamp: performance.now(),
|
|
459
|
-
causes: pendingEntry.causes
|
|
462
|
+
causes: reasonOfUpdate ? detectCauses(fiber) : []
|
|
460
463
|
};
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
entries.push(renderEntry);
|
|
464
|
-
pendingEntries.push(pendingEntry);
|
|
465
|
-
batcher == null ? void 0 : batcher.push(pendingEntry);
|
|
464
|
+
highlightEntries.push(entry);
|
|
465
|
+
highlighter == null ? void 0 : highlighter.push(entry);
|
|
466
466
|
}
|
|
467
|
-
if (
|
|
468
|
-
logRerendersToConsole(
|
|
467
|
+
if (logToConsole) {
|
|
468
|
+
logRerendersToConsole(highlightEntries, reasonOfUpdate);
|
|
469
469
|
}
|
|
470
470
|
};
|
|
471
|
-
|
|
472
|
-
hook.onCommitFiberRoot = previousOnCommit;
|
|
473
|
-
batcher == null ? void 0 : batcher.dispose();
|
|
474
|
-
disposeAllOverlays();
|
|
475
|
-
};
|
|
476
|
-
if (!silent) {
|
|
471
|
+
if (logToConsole) {
|
|
477
472
|
console.log(
|
|
478
473
|
"%c⚛ react-debug-updates attached",
|
|
479
474
|
"color: #61dafb; font-weight: bold"
|
|
480
475
|
);
|
|
481
476
|
}
|
|
482
|
-
return
|
|
477
|
+
return () => {
|
|
478
|
+
hook.onCommitFiberRoot = previousOnCommit;
|
|
479
|
+
highlighter == null ? void 0 : highlighter.dispose();
|
|
480
|
+
disposeAllOverlays();
|
|
481
|
+
};
|
|
483
482
|
}
|
|
484
|
-
exports.
|
|
483
|
+
exports.startReactUpdatesMonitor = startReactUpdatesMonitor;
|
|
485
484
|
//# sourceMappingURL=react-debug-updates.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react-debug-updates.cjs","sources":["../src/fiber.ts","../src/causes.ts","../src/format.ts","../src/overlay.ts","../src/batcher.ts","../src/logger.ts"],"sourcesContent":["import type { Fiber, DetectedRender } from \"./types.js\";\n\n// ────────────────────────────────────────────\n// Fiber tag constants\n// ────────────────────────────────────────────\n\nexport const FiberTag = {\n FunctionComponent: 0,\n ClassComponent: 1,\n HostComponent: 5,\n ForwardRef: 11,\n SimpleMemoComponent: 15,\n MemoComponent: 14,\n} as const;\n\n// Component tags we report as re-renders.\n// MemoComponent (14) is excluded to avoid double-reporting — it's a wrapper\n// fiber around the inner component. The inner component (FunctionComponent,\n// ForwardRef, or SimpleMemoComponent) has its own PerformedWork flag and\n// will be reported correctly on its own.\nconst COMPONENT_TAGS = new Set<number>([\n FiberTag.FunctionComponent,\n FiberTag.ClassComponent,\n FiberTag.ForwardRef,\n FiberTag.SimpleMemoComponent,\n]);\n\nconst PerformedWork = 0b0000001;\n\n// ────────────────────────────────────────────\n// Fiber tree helpers\n// ────────────────────────────────────────────\n\nexport function getComponentName(fiber: Fiber): string | null {\n const { type } = fiber;\n if (!type || typeof type === \"string\") return null;\n return type.displayName ?? type.name ?? null;\n}\n\nfunction isHTMLElement(value: unknown): value is HTMLElement {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as Node).nodeType === 1 &&\n typeof (value as HTMLElement).getBoundingClientRect === \"function\"\n );\n}\n\nexport function findNearestDOMNode(fiber: Fiber): HTMLElement | null {\n if (fiber.tag === FiberTag.HostComponent && isHTMLElement(fiber.stateNode)) {\n return fiber.stateNode;\n }\n let child: Fiber | null = fiber.child;\n while (child) {\n const found = findNearestDOMNode(child);\n if (found) return found;\n child = child.sibling;\n }\n return null;\n}\n\nexport function getFiberPath(fiber: Fiber, maxDepth = 5): string {\n const parts: string[] = [];\n let current: Fiber | null = fiber.return;\n let depth = 0;\n while (current && depth < maxDepth) {\n const name = getComponentName(current);\n if (name) parts.unshift(name);\n current = current.return;\n depth++;\n }\n return parts.length ? parts.join(\" → \") : \"(root)\";\n}\n\n// ────────────────────────────────────────────\n// Self-triggered detection\n// ────────────────────────────────────────────\n\nfunction isSelfTriggered(fiber: Fiber): boolean {\n const alternate = fiber.alternate;\n if (!alternate) return false;\n return fiber.memoizedProps === alternate.memoizedProps;\n}\n\n// ────────────────────────────────────────────\n// didFiberRender — mirrors React DevTools\n// ────────────────────────────────────────────\n//\n// See: react-devtools-shared/src/backend/fiber/shared/DevToolsFiberChangeDetection.js\n//\n// For component fibers (function, class, memo, forwardRef), React sets the\n// PerformedWork flag (bit 0) only when user code actually executes.\n// createWorkInProgress resets flags to NoFlags, so PerformedWork on a\n// work-in-progress fiber is always fresh — never stale from a prior commit.\n//\n// This check must only be called AFTER confirming prevFiber !== nextFiber\n// (i.e. the fiber was actually processed, not a bailed-out subtree).\n\nfunction didFiberRender(nextFiber: Fiber): boolean {\n return (nextFiber.flags & PerformedWork) === PerformedWork;\n}\n\n// ────────────────────────────────────────────\n// Detect which components re-rendered in a commit\n// ────────────────────────────────────────────\n//\n// Mirrors React DevTools' updateFiberRecursively / updateChildrenRecursively.\n//\n// React double-buffers fibers. After a commit, root.current is the committed\n// tree and root.current.alternate is the previous tree. We walk both in\n// parallel. At each level, if prevChild and nextChild are the same object,\n// React bailed out that entire subtree (via cloneChildFibers) — we skip it.\n// Otherwise, we use nextChild.alternate as the previous fiber and check\n// didFiberRender (PerformedWork) to see if user code actually ran.\n//\n// Returns lightweight DetectedRender objects — just the fiber and its depth.\n// DOM lookup, cause detection, and formatting are the caller's responsibility.\n\nexport function detectRenders(\n root: Fiber,\n mode: \"self-triggered\" | \"all\",\n): DetectedRender[] {\n const results: DetectedRender[] = [];\n const selfTriggeredOnly = mode === \"self-triggered\";\n\n // The alternate of the committed root is the previous tree's root.\n // On initial mount this is null — nothing to report.\n const previousRoot = root.alternate;\n if (!previousRoot) return results;\n\n function walk(\n nextFiber: Fiber,\n previousFiber: Fiber | null,\n depth: number,\n ) {\n // ── Check this fiber ──\n if (\n COMPONENT_TAGS.has(nextFiber.tag) &&\n previousFiber !== null &&\n previousFiber !== nextFiber && // same object → bailed-out subtree\n didFiberRender(nextFiber) &&\n (!selfTriggeredOnly || isSelfTriggered(nextFiber))\n ) {\n results.push({ fiber: nextFiber, depth });\n }\n\n // ── Walk children, matching with previous tree ──\n let nextChild = nextFiber.child;\n let previousChildAtSameIndex = previousFiber?.child ?? null;\n\n while (nextChild) {\n let matchedPrevious: Fiber | null;\n\n if (previousChildAtSameIndex === nextChild) {\n // Same object identity — React shared this fiber via cloneChildFibers.\n // The entire subtree was bailed out; passing the same object as both\n // prev and next causes the prevFiber !== nextFiber guard to skip it.\n matchedPrevious = nextChild;\n } else {\n // Different object — this fiber was processed. The alternate is the\n // corresponding fiber from the previous tree.\n matchedPrevious = nextChild.alternate;\n }\n\n walk(nextChild, matchedPrevious, depth + 1);\n\n nextChild = nextChild.sibling;\n previousChildAtSameIndex = previousChildAtSameIndex?.sibling ?? null;\n }\n }\n\n walk(root, previousRoot, 0);\n return results;\n}\n","import type { Fiber, HookNode, UpdateCause } from \"./types.js\";\nimport { FiberTag } from \"./fiber.js\";\n\n// ────────────────────────────────────────────\n// Update cause detection\n// ────────────────────────────────────────────\n//\n// For function components, React stores hooks as a linked list on\n// fiber.memoizedState. In dev builds, _debugHookTypes lists hook names\n// in call order. We walk both in parallel, comparing memoizedState\n// on each node to find which hook's state actually changed.\n//\n// Caveat: useContext does NOT create a linked list node, so we skip it\n// when advancing the pointer, and detect it by elimination.\n\n/**\n * Hooks whose memoizedState changing can trigger a re-render.\n * We only report these — useEffect/useMemo/etc. don't cause re-renders.\n */\nconst STATE_HOOKS = new Set([\n \"useState\",\n \"useReducer\",\n \"useSyncExternalStore\",\n]);\n\n/**\n * Hooks that do NOT create a node in the memoizedState linked list.\n * Currently only useContext — every other hook allocates a node.\n */\nconst HOOKS_WITHOUT_NODE = new Set([\"useContext\"]);\n\nexport function detectCauses(fiber: Fiber): UpdateCause[] {\n const alternate = fiber.alternate;\n if (!alternate) return [];\n\n const causes: UpdateCause[] = [];\n\n // ── Props changed (parent re-rendered us with new props) ──\n if (fiber.memoizedProps !== alternate.memoizedProps) {\n causes.push({ kind: \"props\" });\n }\n\n // ── Class component ──\n if (fiber.tag === FiberTag.ClassComponent) {\n if (fiber.memoizedState !== alternate.memoizedState) {\n causes.push({ kind: \"class-state\" });\n }\n return causes;\n }\n\n // ── Function component hooks ──\n const hookTypes = fiber._debugHookTypes;\n\n if (!hookTypes) {\n // No debug info (prod build) — best effort\n if (fiber.memoizedState !== alternate.memoizedState) {\n causes.push({ kind: \"unknown\" });\n }\n return causes;\n }\n\n let currentNode = fiber.memoizedState as HookNode | null;\n let previousNode = alternate.memoizedState as HookNode | null;\n let hasContextHook = false;\n let anyStateHookChanged = false;\n\n for (let i = 0; i < hookTypes.length; i++) {\n const type = hookTypes[i];\n\n // useContext has no linked list node — skip pointer advance\n if (HOOKS_WITHOUT_NODE.has(type)) {\n if (type === \"useContext\") hasContextHook = true;\n continue;\n }\n\n if (STATE_HOOKS.has(type) && currentNode && previousNode) {\n if (!Object.is(currentNode.memoizedState, previousNode.memoizedState)) {\n anyStateHookChanged = true;\n causes.push({\n kind: \"hook\",\n hookIndex: i,\n hookType: type,\n previousValue: previousNode.memoizedState,\n nextValue: currentNode.memoizedState,\n });\n }\n }\n\n currentNode = currentNode?.next ?? null;\n previousNode = previousNode?.next ?? null;\n }\n\n // If no state hook changed but component is self-triggered and has\n // useContext → the context value must have changed.\n if (\n hasContextHook &&\n !anyStateHookChanged &&\n fiber.memoizedProps === alternate.memoizedProps\n ) {\n causes.push({ kind: \"hook\", hookType: \"useContext\" });\n }\n\n // If still nothing and self-triggered, mark unknown\n if (causes.length === 0 && fiber.memoizedProps === alternate.memoizedProps) {\n causes.push({ kind: \"unknown\" });\n }\n\n return causes;\n}\n","import type { PendingEntry, UpdateCause } from \"./types.js\";\n\n// ────────────────────────────────────────────\n// Value formatting\n// ────────────────────────────────────────────\n\nexport function formatValue(value: unknown, maxLength = 50): string {\n if (value === null) return \"null\";\n if (value === undefined) return \"undefined\";\n if (typeof value === \"boolean\") return String(value);\n if (typeof value === \"number\") return String(value);\n if (typeof value === \"function\")\n return `ƒ ${(value as { name?: string }).name || \"anonymous\"}`;\n\n if (typeof value === \"string\") {\n const truncated =\n value.length > maxLength ? value.slice(0, maxLength) + \"…\" : value;\n return JSON.stringify(truncated);\n }\n\n if (Array.isArray(value)) return `Array(${value.length})`;\n\n if (typeof value === \"object\") {\n const name = (value as object).constructor?.name;\n if (name && name !== \"Object\") return name;\n const keys = Object.keys(value as object);\n if (keys.length <= 3) return `{ ${keys.join(\", \")} }`;\n return `{ ${keys.slice(0, 3).join(\", \")}, … }`;\n }\n\n return String(value);\n}\n\n// ────────────────────────────────────────────\n// Cause formatting\n// ────────────────────────────────────────────\n\n/** Compact summary for overlay labels. */\nexport function formatCausesShort(causes: UpdateCause[]): string {\n if (causes.length === 0) return \"\";\n\n const parts: string[] = [];\n for (const cause of causes) {\n if (cause.kind === \"props\") {\n parts.push(\"props\");\n } else if (cause.kind === \"class-state\") {\n parts.push(\"state\");\n } else if (cause.kind === \"hook\" && cause.hookType) {\n const indexSuffix = cause.hookIndex != null ? `[${cause.hookIndex}]` : \"\";\n parts.push(`${cause.hookType}${indexSuffix}`);\n } else {\n parts.push(\"?\");\n }\n }\n\n return parts.join(\", \");\n}\n\n/** Detailed lines for console output. */\nexport function formatCausesConsole(causes: UpdateCause[]): string[] {\n const lines: string[] = [];\n\n for (const cause of causes) {\n if (cause.kind === \"props\") {\n lines.push(\" ↳ props changed (parent re-rendered)\");\n } else if (cause.kind === \"class-state\") {\n lines.push(\" ↳ class state changed\");\n } else if (cause.kind === \"hook\" && cause.hookType) {\n const indexSuffix =\n cause.hookIndex != null ? `[${cause.hookIndex}]` : \"\";\n const name = `${cause.hookType}${indexSuffix}`;\n\n if (cause.previousValue !== undefined && cause.nextValue !== undefined) {\n lines.push(\n ` ↳ ${name}: ${formatValue(cause.previousValue)} → ${formatValue(cause.nextValue)}`,\n );\n } else {\n lines.push(` ↳ ${name} changed`);\n }\n } else {\n lines.push(\" ↳ unknown cause\");\n }\n }\n\n return lines;\n}\n\n// ────────────────────────────────────────────\n// Console output\n// ────────────────────────────────────────────\n\n/** Log re-renders as a collapsed console group. */\nexport function logRerendersToConsole(\n entries: PendingEntry[],\n showCauses: boolean,\n) {\n if (entries.length === 0) return;\n\n console.groupCollapsed(\n `%c⚛ ${entries.length} re-render${entries.length > 1 ? \"s\" : \"\"}`,\n \"color: #61dafb; font-weight: bold\",\n );\n\n for (let i = 0; i < entries.length; i++) {\n const entry = entries[i];\n const durationText =\n entry.duration > 0 ? ` (${entry.duration.toFixed(2)}ms)` : \"\";\n console.log(\n `%c${entry.component}%c ${entry.path}${durationText}`,\n \"color: #e8e82e; font-weight: bold\",\n \"color: #888\",\n );\n\n if (showCauses && entry.causes.length > 0) {\n const lines = formatCausesConsole(entry.causes);\n for (const line of lines) {\n console.log(`%c${line}`, \"color: #aaa\");\n }\n }\n }\n\n console.groupEnd();\n}\n","// ────────────────────────────────────────────\n// Per-window overlay infrastructure\n// ────────────────────────────────────────────\n//\n// Performance:\n// - Overlay elements pooled per window, reused via animationend\n// - Single CSS @keyframes per window, no JS timers per element\n\nconst ANIMATION_NAME = \"__rdu-fade\";\n\nconst injectedWindows = new WeakSet<Window>();\n\nfunction ensureStylesheet(win: Window) {\n if (injectedWindows.has(win)) return;\n injectedWindows.add(win);\n\n const style = win.document.createElement(\"style\");\n style.textContent = `\n @keyframes ${ANIMATION_NAME} {\n 0% { opacity: 0; }\n 8% { opacity: var(--rdu-opacity, 1); }\n 40% { opacity: var(--rdu-opacity, 1); }\n 100% { opacity: 0; }\n }\n .${ANIMATION_NAME}-box {\n position: fixed;\n pointer-events: none;\n box-sizing: border-box;\n border-radius: 3px;\n opacity: 0;\n will-change: opacity;\n }\n .${ANIMATION_NAME}-label {\n position: absolute;\n top: -18px;\n left: -1px;\n font: 10px/16px ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace;\n padding: 0 4px;\n color: #fff;\n border-radius: 2px 2px 0 0;\n white-space: nowrap;\n pointer-events: none;\n }\n `;\n win.document.head.appendChild(style);\n}\n\n// ────────────────────────────────────────────\n// Overlay root container\n// ────────────────────────────────────────────\n\nconst overlayRoots = new WeakMap<Window, HTMLDivElement>();\n\nfunction getOverlayRoot(win: Window): HTMLDivElement | null {\n if (win.closed) return null;\n\n const existing = overlayRoots.get(win);\n if (existing?.isConnected) return existing;\n\n ensureStylesheet(win);\n\n const root = win.document.createElement(\"div\");\n root.id = \"__react-debug-updates\";\n Object.assign(root.style, {\n position: \"fixed\",\n top: \"0\",\n left: \"0\",\n width: \"0\",\n height: \"0\",\n overflow: \"visible\",\n pointerEvents: \"none\",\n zIndex: \"2147483647\",\n } satisfies Partial<CSSStyleDeclaration>);\n win.document.body.appendChild(root);\n\n overlayRoots.set(win, root);\n return root;\n}\n\n// ────────────────────────────────────────────\n// Element pool\n// ────────────────────────────────────────────\n\nconst pools = new WeakMap<Window, HTMLDivElement[]>();\n\nexport function acquireOverlay(win: Window): HTMLDivElement | null {\n const root = getOverlayRoot(win);\n if (!root) return null;\n\n let pool = pools.get(win);\n if (!pool) {\n pool = [];\n pools.set(win, pool);\n }\n\n const document = win.document;\n let element = pool.pop();\n\n if (!element) {\n element = document.createElement(\"div\");\n element.className = `${ANIMATION_NAME}-box`;\n\n const label = document.createElement(\"span\");\n label.className = `${ANIMATION_NAME}-label`;\n element.appendChild(label);\n\n element.addEventListener(\"animationend\", () => {\n element!.style.animation = \"none\";\n element!.remove();\n pool!.push(element!);\n });\n }\n\n root.appendChild(element);\n return element;\n}\n\nexport function disposeAllOverlays() {\n const mainRoot = overlayRoots.get(window);\n mainRoot?.remove();\n overlayRoots.delete(window);\n pools.delete(window);\n}\n\nexport const OVERLAY_ANIMATION_NAME = ANIMATION_NAME;\n","import type { HighlightOptions, PendingEntry } from \"./types.js\";\nimport { formatCausesShort } from \"./format.js\";\nimport { acquireOverlay, OVERLAY_ANIMATION_NAME } from \"./overlay.js\";\n\n// ────────────────────────────────────────────\n// Heat color\n// ────────────────────────────────────────────\n\nfunction heatColor(count: number, alpha: number): string {\n const normalizedCount = Math.min((count - 1) / 7, 1);\n const hue = 200 - normalizedCount * 200;\n const saturation = 85 + normalizedCount * 15;\n const lightness = 55 - normalizedCount * 10;\n return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`;\n}\n\n// ────────────────────────────────────────────\n// Batched flush\n// ────────────────────────────────────────────\n//\n// Commit path just pushes lightweight refs (no DOM reads, no formatting).\n// Flush (setInterval): batched rect reads → batched DOM writes.\n\nexport const HIGHLIGHT_DEFAULTS: Required<HighlightOptions> = {\n flushInterval: 250,\n animationDuration: 1200,\n showLabels: true,\n opacity: 0.3,\n};\n\ninterface CoalescedEntry {\n count: number;\n totalDuration: number;\n component: string;\n /** Shallowest fiber depth among coalesced entries (for z-ordering). */\n depth: number;\n domNode: HTMLElement;\n ownerWindow: Window;\n causeSummary: string;\n}\n\nexport function createBatcher(options: Required<HighlightOptions>) {\n let pending: PendingEntry[] = [];\n let timer: ReturnType<typeof setInterval> | null = null;\n\n function flush() {\n if (pending.length === 0) return;\n\n const batch = pending;\n pending = [];\n\n // Coalesce by DOM node identity\n const map = new Map<HTMLElement, CoalescedEntry>();\n\n for (let i = 0; i < batch.length; i++) {\n const entry = batch[i];\n if (!entry.domNode) continue;\n\n const existing = map.get(entry.domNode);\n if (existing) {\n existing.count++;\n existing.totalDuration += entry.duration;\n existing.depth = Math.min(existing.depth, entry.depth);\n } else {\n const win = entry.domNode.ownerDocument?.defaultView;\n if (!win || win.closed) continue;\n map.set(entry.domNode, {\n count: 1,\n totalDuration: entry.duration,\n component: entry.component,\n depth: entry.depth,\n domNode: entry.domNode,\n ownerWindow: win,\n causeSummary: formatCausesShort(entry.causes),\n });\n }\n }\n\n // Read phase: batch all rect reads\n const toShow: Array<{ coalesced: CoalescedEntry; rect: DOMRect }> = [];\n for (const coalesced of map.values()) {\n const rect = coalesced.domNode.getBoundingClientRect();\n if (rect.width > 0 || rect.height > 0) {\n toShow.push({ coalesced, rect });\n }\n }\n\n // Sort deepest first so parents are appended last (render on top)\n toShow.sort((a, b) => b.coalesced.depth - a.coalesced.depth);\n\n // Write phase: position overlays\n for (let i = 0; i < toShow.length; i++) {\n const { coalesced, rect } = toShow[i];\n const element = acquireOverlay(coalesced.ownerWindow);\n if (!element) continue;\n\n const fillColor = heatColor(coalesced.count, 0.18);\n const borderColor = heatColor(coalesced.count, 0.75);\n const labelBackground = heatColor(coalesced.count, 0.9);\n\n const style = element.style;\n style.top = `${rect.top}px`;\n style.left = `${rect.left}px`;\n style.width = `${rect.width}px`;\n style.height = `${rect.height}px`;\n style.backgroundColor = fillColor;\n style.border = `1.5px solid ${borderColor}`;\n style.setProperty(\"--rdu-opacity\", String(options.opacity));\n style.animation = `${OVERLAY_ANIMATION_NAME} ${options.animationDuration}ms ease-out forwards`;\n\n const label = element.firstElementChild as HTMLElement;\n if (options.showLabels) {\n const countText = coalesced.count > 1 ? ` ×${coalesced.count}` : \"\";\n const durationText =\n coalesced.totalDuration > 0\n ? ` ${coalesced.totalDuration.toFixed(1)}ms`\n : \"\";\n const causeText = coalesced.causeSummary\n ? ` (${coalesced.causeSummary})`\n : \"\";\n label.textContent = `${coalesced.component}${countText}${durationText}${causeText}`;\n label.style.backgroundColor = labelBackground;\n } else {\n label.textContent = \"\";\n label.style.backgroundColor = \"transparent\";\n }\n }\n }\n\n function push(entry: PendingEntry) {\n pending.push(entry);\n if (!timer) {\n timer = setInterval(flush, options.flushInterval);\n }\n }\n\n function dispose() {\n if (timer) {\n clearInterval(timer);\n timer = null;\n }\n pending = [];\n }\n\n return { push, dispose };\n}\n","import type {\n DevToolsHook,\n FiberRoot,\n LoggerOptions,\n PendingEntry,\n RenderEntry,\n RenderLogger,\n} from \"./types.js\";\nimport {\n detectRenders,\n findNearestDOMNode,\n getComponentName,\n getFiberPath,\n} from \"./fiber.js\";\nimport { detectCauses } from \"./causes.js\";\nimport { logRerendersToConsole } from \"./format.js\";\nimport { HIGHLIGHT_DEFAULTS, createBatcher } from \"./batcher.js\";\nimport { disposeAllOverlays } from \"./overlay.js\";\n\n/**\n * Attach a render logger to React's DevTools hook.\n *\n * Hooks into `__REACT_DEVTOOLS_GLOBAL_HOOK__.onCommitFiberRoot` to intercept\n * every React commit. Records re-render entries, optionally logs them to the\n * console, and optionally shows visual highlight overlays on re-rendered DOM nodes.\n *\n * Returns a `RenderLogger` handle, or `null` if the DevTools hook is not found.\n */\nexport function attachRenderLogger(\n options: LoggerOptions = {},\n): RenderLogger | null {\n const {\n silent = false,\n bufferSize = 500,\n filter,\n highlight = false,\n mode = \"self-triggered\",\n showCauses = false,\n } = options;\n\n const hook = (\n window as unknown as { __REACT_DEVTOOLS_GLOBAL_HOOK__?: DevToolsHook }\n ).__REACT_DEVTOOLS_GLOBAL_HOOK__;\n\n if (!hook) {\n console.warn(\n \"[react-debug-updates] __REACT_DEVTOOLS_GLOBAL_HOOK__ not found. \" +\n \"Make sure React DevTools or a dev build of React is active.\",\n );\n return null;\n }\n\n const highlightOptions = highlight\n ? {\n ...HIGHLIGHT_DEFAULTS,\n ...(typeof highlight === \"object\" ? highlight : {}),\n }\n : null;\n\n const batcher = highlightOptions ? createBatcher(highlightOptions) : null;\n\n const entries: RenderEntry[] = [];\n const previousOnCommit = hook.onCommitFiberRoot.bind(hook);\n\n hook.onCommitFiberRoot = (\n rendererID: number,\n root: FiberRoot,\n priorityLevel?: unknown,\n ) => {\n previousOnCommit(rendererID, root, priorityLevel);\n\n // 1. Detect which component fibers actually re-rendered (pure fiber analysis)\n const detectedRenders = detectRenders(root.current, mode);\n if (detectedRenders.length === 0) return;\n\n // 2. Build full entries: resolve names, DOM nodes, causes\n const pendingEntries: PendingEntry[] = [];\n\n for (let i = 0; i < detectedRenders.length; i++) {\n const { fiber, depth } = detectedRenders[i];\n const name = getComponentName(fiber);\n if (!name) continue;\n\n const pendingEntry: PendingEntry = {\n component: name,\n path: getFiberPath(fiber),\n duration: fiber.actualDuration ?? 0,\n depth,\n domNode: findNearestDOMNode(fiber),\n causes: showCauses ? detectCauses(fiber) : [],\n };\n\n const renderEntry: RenderEntry = {\n component: pendingEntry.component,\n path: pendingEntry.path,\n duration: pendingEntry.duration,\n timestamp: performance.now(),\n causes: pendingEntry.causes,\n };\n\n if (filter && !filter(renderEntry)) continue;\n\n if (entries.length >= bufferSize) entries.shift();\n entries.push(renderEntry);\n\n pendingEntries.push(pendingEntry);\n batcher?.push(pendingEntry);\n }\n\n // 3. Console output\n if (!silent) {\n logRerendersToConsole(pendingEntries, showCauses);\n }\n };\n\n const disconnect = () => {\n hook.onCommitFiberRoot = previousOnCommit;\n batcher?.dispose();\n disposeAllOverlays();\n };\n\n if (!silent) {\n console.log(\n \"%c⚛ react-debug-updates attached\",\n \"color: #61dafb; font-weight: bold\",\n );\n }\n\n return { entries, disconnect };\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"react-debug-updates.cjs","sources":["../src/fiber.ts","../src/causes.ts","../src/format.ts","../src/overlay.ts","../src/highlights.ts","../src/monitor.ts"],"sourcesContent":["import type { Fiber, DetectedRender } from \"./types.js\";\n\n// ────────────────────────────────────────────\n// Fiber tag constants\n// ────────────────────────────────────────────\n\nexport const FiberTag = {\n FunctionComponent: 0,\n ClassComponent: 1,\n HostComponent: 5,\n ForwardRef: 11,\n SimpleMemoComponent: 15,\n MemoComponent: 14,\n} as const;\n\n// Component tags we report as re-renders.\n// MemoComponent (14) is excluded to avoid double-reporting — it's a wrapper\n// fiber around the inner component. The inner component (FunctionComponent,\n// ForwardRef, or SimpleMemoComponent) has its own PerformedWork flag and\n// will be reported correctly on its own.\nconst COMPONENT_TAGS = new Set<number>([\n FiberTag.FunctionComponent,\n FiberTag.ClassComponent,\n FiberTag.ForwardRef,\n FiberTag.SimpleMemoComponent,\n]);\n\nconst PerformedWork = 0b0000001;\n\n// ────────────────────────────────────────────\n// Fiber tree helpers\n// ────────────────────────────────────────────\n\nexport function getComponentName(fiber: Fiber): string | null {\n const { type } = fiber;\n if (!type || typeof type === \"string\") return null;\n return type.displayName ?? type.name ?? null;\n}\n\nfunction isHTMLElement(value: unknown): value is HTMLElement {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as Node).nodeType === 1 &&\n typeof (value as HTMLElement).getBoundingClientRect === \"function\"\n );\n}\n\nexport function findNearestDOMNode(fiber: Fiber): HTMLElement | null {\n if (fiber.tag === FiberTag.HostComponent && isHTMLElement(fiber.stateNode)) {\n return fiber.stateNode;\n }\n let child: Fiber | null = fiber.child;\n while (child) {\n const found = findNearestDOMNode(child);\n if (found) return found;\n child = child.sibling;\n }\n return null;\n}\n\nexport function getFiberPath(fiber: Fiber, maxDepth = 5): string {\n const parts: string[] = [];\n let current: Fiber | null = fiber.return;\n let depth = 0;\n while (current && depth < maxDepth) {\n const name = getComponentName(current);\n if (name) parts.unshift(name);\n current = current.return;\n depth++;\n }\n return parts.length ? parts.join(\" → \") : \"(root)\";\n}\n\n// ────────────────────────────────────────────\n// Self-triggered detection\n// ────────────────────────────────────────────\n\nfunction isSelfTriggered(fiber: Fiber): boolean {\n const alternate = fiber.alternate;\n if (!alternate) return false;\n return fiber.memoizedProps === alternate.memoizedProps;\n}\n\n// ────────────────────────────────────────────\n// didFiberRender — mirrors React DevTools\n// ────────────────────────────────────────────\n//\n// See: react-devtools-shared/src/backend/fiber/shared/DevToolsFiberChangeDetection.js\n//\n// For component fibers (function, class, memo, forwardRef), React sets the\n// PerformedWork flag (bit 0) only when user code actually executes.\n// createWorkInProgress resets flags to NoFlags, so PerformedWork on a\n// work-in-progress fiber is always fresh — never stale from a prior commit.\n//\n// This check must only be called AFTER confirming prevFiber !== nextFiber\n// (i.e. the fiber was actually processed, not a bailed-out subtree).\n\nfunction didFiberRender(nextFiber: Fiber): boolean {\n return (nextFiber.flags & PerformedWork) === PerformedWork;\n}\n\n// ────────────────────────────────────────────\n// Detect which components re-rendered in a commit\n// ────────────────────────────────────────────\n//\n// Mirrors React DevTools' updateFiberRecursively / updateChildrenRecursively.\n//\n// React double-buffers fibers. After a commit, root.current is the committed\n// tree and root.current.alternate is the previous tree. We walk both in\n// parallel. At each level, if prevChild and nextChild are the same object,\n// React bailed out that entire subtree (via cloneChildFibers) — we skip it.\n// Otherwise, we use nextChild.alternate as the previous fiber and check\n// didFiberRender (PerformedWork) to see if user code actually ran.\n//\n// Returns lightweight DetectedRender objects — just the fiber and its depth.\n// DOM lookup, cause detection, and formatting are the caller's responsibility.\n\nexport function detectRenders(\n root: Fiber,\n mode: \"self-triggered\" | \"all\",\n): DetectedRender[] {\n const results: DetectedRender[] = [];\n const selfTriggeredOnly = mode === \"self-triggered\";\n\n // The alternate of the committed root is the previous tree's root.\n // On initial mount this is null — nothing to report.\n const previousRoot = root.alternate;\n if (!previousRoot) return results;\n\n function walk(\n nextFiber: Fiber,\n previousFiber: Fiber | null,\n depth: number,\n ) {\n // ── Check this fiber ──\n if (\n COMPONENT_TAGS.has(nextFiber.tag) &&\n previousFiber !== null &&\n previousFiber !== nextFiber && // same object → bailed-out subtree\n didFiberRender(nextFiber) &&\n (!selfTriggeredOnly || isSelfTriggered(nextFiber))\n ) {\n results.push({ fiber: nextFiber, depth });\n }\n\n // ── Walk children, matching with previous tree ──\n let nextChild = nextFiber.child;\n let previousChildAtSameIndex = previousFiber?.child ?? null;\n\n while (nextChild) {\n let matchedPrevious: Fiber | null;\n\n if (previousChildAtSameIndex === nextChild) {\n // Same object identity — React shared this fiber via cloneChildFibers.\n // The entire subtree was bailed out; passing the same object as both\n // prev and next causes the prevFiber !== nextFiber guard to skip it.\n matchedPrevious = nextChild;\n } else {\n // Different object — this fiber was processed. The alternate is the\n // corresponding fiber from the previous tree.\n matchedPrevious = nextChild.alternate;\n }\n\n walk(nextChild, matchedPrevious, depth + 1);\n\n nextChild = nextChild.sibling;\n previousChildAtSameIndex = previousChildAtSameIndex?.sibling ?? null;\n }\n }\n\n walk(root, previousRoot, 0);\n return results;\n}\n","import type { Fiber, HookNode, UpdateCause } from \"./types.js\";\nimport { FiberTag } from \"./fiber.js\";\n\n// ────────────────────────────────────────────\n// Update cause detection\n// ────────────────────────────────────────────\n//\n// For function components, React stores hooks as a linked list on\n// fiber.memoizedState. In dev builds, _debugHookTypes lists hook names\n// in call order. We walk both in parallel, comparing memoizedState\n// on each node to find which hook's state actually changed.\n//\n// Caveat: useContext does NOT create a linked list node, so we skip it\n// when advancing the pointer, and detect it by elimination.\n\n/**\n * Hooks whose memoizedState changing can trigger a re-render.\n * We only report these — useEffect/useMemo/etc. don't cause re-renders.\n */\nconst STATE_HOOKS = new Set([\n \"useState\",\n \"useReducer\",\n \"useSyncExternalStore\",\n]);\n\n/**\n * Hooks that do NOT create a node in the memoizedState linked list.\n * Currently only useContext — every other hook allocates a node.\n */\nconst HOOKS_WITHOUT_NODE = new Set([\"useContext\"]);\n\nexport function detectCauses(fiber: Fiber): UpdateCause[] {\n const alternate = fiber.alternate;\n if (!alternate) return [];\n\n const causes: UpdateCause[] = [];\n\n // ── Props changed (parent re-rendered us with new props) ──\n if (fiber.memoizedProps !== alternate.memoizedProps) {\n causes.push({ kind: \"props\" });\n }\n\n // ── Class component ──\n if (fiber.tag === FiberTag.ClassComponent) {\n if (fiber.memoizedState !== alternate.memoizedState) {\n causes.push({ kind: \"class-state\" });\n }\n return causes;\n }\n\n // ── Function component hooks ──\n const hookTypes = fiber._debugHookTypes;\n\n if (!hookTypes) {\n // No debug info (prod build) — best effort\n if (fiber.memoizedState !== alternate.memoizedState) {\n causes.push({ kind: \"unknown\" });\n }\n return causes;\n }\n\n let currentNode = fiber.memoizedState as HookNode | null;\n let previousNode = alternate.memoizedState as HookNode | null;\n let hasContextHook = false;\n let anyStateHookChanged = false;\n\n for (let i = 0; i < hookTypes.length; i++) {\n const type = hookTypes[i];\n\n // useContext has no linked list node — skip pointer advance\n if (HOOKS_WITHOUT_NODE.has(type)) {\n if (type === \"useContext\") hasContextHook = true;\n continue;\n }\n\n if (STATE_HOOKS.has(type) && currentNode && previousNode) {\n if (!Object.is(currentNode.memoizedState, previousNode.memoizedState)) {\n anyStateHookChanged = true;\n causes.push({\n kind: \"hook\",\n hookIndex: i,\n hookType: type,\n previousValue: previousNode.memoizedState,\n nextValue: currentNode.memoizedState,\n });\n }\n }\n\n currentNode = currentNode?.next ?? null;\n previousNode = previousNode?.next ?? null;\n }\n\n // If no state hook changed but component is self-triggered and has\n // useContext → the context value must have changed.\n if (\n hasContextHook &&\n !anyStateHookChanged &&\n fiber.memoizedProps === alternate.memoizedProps\n ) {\n causes.push({ kind: \"hook\", hookType: \"useContext\" });\n }\n\n // If still nothing and self-triggered, mark unknown\n if (causes.length === 0 && fiber.memoizedProps === alternate.memoizedProps) {\n causes.push({ kind: \"unknown\" });\n }\n\n return causes;\n}\n","import type { HighlightEntry, UpdateCause } from \"./types.js\";\n\n// ────────────────────────────────────────────\n// Value formatting\n// ────────────────────────────────────────────\n\nexport function formatValue(value: unknown, maxLength = 50): string {\n if (value === null) return \"null\";\n if (value === undefined) return \"undefined\";\n if (typeof value === \"boolean\") return String(value);\n if (typeof value === \"number\") return String(value);\n if (typeof value === \"function\")\n return `ƒ ${(value as { name?: string }).name || \"anonymous\"}`;\n\n if (typeof value === \"string\") {\n const truncated =\n value.length > maxLength ? value.slice(0, maxLength) + \"…\" : value;\n return JSON.stringify(truncated);\n }\n\n if (Array.isArray(value)) return `Array(${value.length})`;\n\n if (typeof value === \"object\") {\n const name = (value as object).constructor?.name;\n if (name && name !== \"Object\") return name;\n const keys = Object.keys(value as object);\n if (keys.length <= 3) return `{ ${keys.join(\", \")} }`;\n return `{ ${keys.slice(0, 3).join(\", \")}, … }`;\n }\n\n return String(value);\n}\n\n// ────────────────────────────────────────────\n// Cause formatting\n// ────────────────────────────────────────────\n\n/** Compact summary for overlay labels. */\nexport function formatCausesShort(causes: UpdateCause[]): string {\n if (causes.length === 0) return \"\";\n\n const parts: string[] = [];\n for (const cause of causes) {\n if (cause.kind === \"props\") {\n parts.push(\"props\");\n } else if (cause.kind === \"class-state\") {\n parts.push(\"state\");\n } else if (cause.kind === \"hook\" && cause.hookType) {\n const indexSuffix = cause.hookIndex != null ? `[${cause.hookIndex}]` : \"\";\n parts.push(`${cause.hookType}${indexSuffix}`);\n } else {\n parts.push(\"?\");\n }\n }\n\n return parts.join(\", \");\n}\n\n/** Detailed lines for console output. */\nexport function formatCausesConsole(causes: UpdateCause[]): string[] {\n const lines: string[] = [];\n\n for (const cause of causes) {\n if (cause.kind === \"props\") {\n lines.push(\" ↳ props changed (parent re-rendered)\");\n } else if (cause.kind === \"class-state\") {\n lines.push(\" ↳ class state changed\");\n } else if (cause.kind === \"hook\" && cause.hookType) {\n const indexSuffix =\n cause.hookIndex != null ? `[${cause.hookIndex}]` : \"\";\n const name = `${cause.hookType}${indexSuffix}`;\n\n if (cause.previousValue !== undefined && cause.nextValue !== undefined) {\n lines.push(\n ` ↳ ${name}: ${formatValue(cause.previousValue)} → ${formatValue(cause.nextValue)}`,\n );\n } else {\n lines.push(` ↳ ${name} changed`);\n }\n } else {\n lines.push(\" ↳ unknown cause\");\n }\n }\n\n return lines;\n}\n\n// ────────────────────────────────────────────\n// Console output\n// ────────────────────────────────────────────\n\n/** Log re-renders as a collapsed console group. */\nexport function logRerendersToConsole(\n entries: HighlightEntry[],\n showCauses: boolean,\n) {\n if (entries.length === 0) return;\n\n console.groupCollapsed(\n `%c⚛ ${entries.length} re-render${entries.length > 1 ? \"s\" : \"\"}`,\n \"color: #61dafb; font-weight: bold\",\n );\n\n for (let i = 0; i < entries.length; i++) {\n const entry = entries[i];\n const durationText =\n entry.duration > 0 ? ` (${entry.duration.toFixed(2)}ms)` : \"\";\n console.log(\n `%c${entry.component}%c ${entry.path}${durationText}`,\n \"color: #e8e82e; font-weight: bold\",\n \"color: #888\",\n );\n\n if (showCauses && entry.causes.length > 0) {\n const lines = formatCausesConsole(entry.causes);\n for (const line of lines) {\n console.log(`%c${line}`, \"color: #aaa\");\n }\n }\n }\n\n console.groupEnd();\n}\n","// ────────────────────────────────────────────\n// Per-window overlay infrastructure\n// ────────────────────────────────────────────\n//\n// Performance:\n// - Overlay elements pooled per window, reused via animationend\n// - Single CSS @keyframes per window, no JS timers per element\n\nconst ANIMATION_NAME = \"__rdu-fade\";\n\nconst injectedWindows = new WeakSet<Window>();\n\nfunction ensureStylesheet(win: Window) {\n if (injectedWindows.has(win)) return;\n injectedWindows.add(win);\n\n const style = win.document.createElement(\"style\");\n style.textContent = `\n @keyframes ${ANIMATION_NAME} {\n 0% { opacity: 0; }\n 8% { opacity: var(--rdu-opacity, 1); }\n 40% { opacity: var(--rdu-opacity, 1); }\n 100% { opacity: 0; }\n }\n .${ANIMATION_NAME}-box {\n position: fixed;\n pointer-events: none;\n box-sizing: border-box;\n border-radius: 3px;\n opacity: 0;\n will-change: opacity;\n }\n .${ANIMATION_NAME}-label {\n position: absolute;\n top: -18px;\n left: -1px;\n font: 10px/16px ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace;\n padding: 0 4px;\n color: #fff;\n border-radius: 2px 2px 0 0;\n white-space: nowrap;\n pointer-events: none;\n }\n `;\n win.document.head.appendChild(style);\n}\n\n// ────────────────────────────────────────────\n// Overlay root container\n// ────────────────────────────────────────────\n\nconst overlayRoots = new WeakMap<Window, HTMLDivElement>();\n\nfunction getOverlayRoot(win: Window): HTMLDivElement | null {\n if (win.closed) return null;\n\n const existing = overlayRoots.get(win);\n if (existing?.isConnected) return existing;\n\n ensureStylesheet(win);\n\n const root = win.document.createElement(\"div\");\n root.id = \"__react-debug-updates\";\n Object.assign(root.style, {\n position: \"fixed\",\n top: \"0\",\n left: \"0\",\n width: \"0\",\n height: \"0\",\n overflow: \"visible\",\n pointerEvents: \"none\",\n zIndex: \"2147483647\",\n } satisfies Partial<CSSStyleDeclaration>);\n win.document.body.appendChild(root);\n\n overlayRoots.set(win, root);\n return root;\n}\n\n// ────────────────────────────────────────────\n// Element pool\n// ────────────────────────────────────────────\n\nconst pools = new WeakMap<Window, HTMLDivElement[]>();\n\nexport function acquireOverlay(win: Window): HTMLDivElement | null {\n const root = getOverlayRoot(win);\n if (!root) return null;\n\n let pool = pools.get(win);\n if (!pool) {\n pool = [];\n pools.set(win, pool);\n }\n\n const document = win.document;\n let element = pool.pop();\n\n if (!element) {\n element = document.createElement(\"div\");\n element.className = `${ANIMATION_NAME}-box`;\n\n const label = document.createElement(\"span\");\n label.className = `${ANIMATION_NAME}-label`;\n element.appendChild(label);\n\n element.addEventListener(\"animationend\", () => {\n element!.style.animation = \"none\";\n element!.remove();\n pool!.push(element!);\n });\n }\n\n root.appendChild(element);\n return element;\n}\n\nexport function disposeAllOverlays() {\n const mainRoot = overlayRoots.get(window);\n mainRoot?.remove();\n overlayRoots.delete(window);\n pools.delete(window);\n}\n\nexport const OVERLAY_ANIMATION_NAME = ANIMATION_NAME;\n","import type { OverlayConfig, HighlightEntry } from \"./types.js\";\nimport { formatCausesShort } from \"./format.js\";\nimport { acquireOverlay, OVERLAY_ANIMATION_NAME } from \"./overlay.js\";\n\n// ────────────────────────────────────────────\n// Heat color\n// ────────────────────────────────────────────\n\nfunction heatColor(count: number, alpha: number): string {\n const normalizedCount = Math.min((count - 1) / 7, 1);\n const hue = 200 - normalizedCount * 200;\n const saturation = 85 + normalizedCount * 15;\n const lightness = 55 - normalizedCount * 10;\n return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`;\n}\n\n// ────────────────────────────────────────────\n// Highlight scheduler\n// ────────────────────────────────────────────\n//\n// Commit path just pushes lightweight entries (no DOM reads, no formatting).\n// Flush (setInterval): batched rect reads → batched DOM writes.\n\ninterface CoalescedEntry {\n count: number;\n totalDuration: number;\n component: string;\n /** Shallowest fiber depth among coalesced entries (for z-ordering). */\n depth: number;\n domNode: HTMLElement;\n ownerWindow: Window;\n causeSummary: string;\n}\n\nexport function createHighlighter(config: OverlayConfig) {\n let pending: HighlightEntry[] = [];\n let timer: ReturnType<typeof setInterval> | null = null;\n\n function flush() {\n if (pending.length === 0) return;\n\n const batch = pending;\n pending = [];\n\n // Coalesce by DOM node identity\n const map = new Map<HTMLElement, CoalescedEntry>();\n\n for (let i = 0; i < batch.length; i++) {\n const entry = batch[i];\n if (!entry.domNode) continue;\n\n const existing = map.get(entry.domNode);\n if (existing) {\n existing.count++;\n existing.totalDuration += entry.duration;\n existing.depth = Math.min(existing.depth, entry.depth);\n } else {\n const win = entry.domNode.ownerDocument?.defaultView;\n if (!win || win.closed) continue;\n map.set(entry.domNode, {\n count: 1,\n totalDuration: entry.duration,\n component: entry.component,\n depth: entry.depth,\n domNode: entry.domNode,\n ownerWindow: win,\n causeSummary: formatCausesShort(entry.causes),\n });\n }\n }\n\n // Read phase: batch all rect reads\n const toShow: Array<{ coalesced: CoalescedEntry; rect: DOMRect }> = [];\n for (const coalesced of map.values()) {\n const rect = coalesced.domNode.getBoundingClientRect();\n if (rect.width > 0 || rect.height > 0) {\n toShow.push({ coalesced, rect });\n }\n }\n\n // Sort deepest first so parents are appended last (render on top)\n toShow.sort((a, b) => b.coalesced.depth - a.coalesced.depth);\n\n // Write phase: position overlays\n for (let i = 0; i < toShow.length; i++) {\n const { coalesced, rect } = toShow[i];\n const element = acquireOverlay(coalesced.ownerWindow);\n if (!element) continue;\n\n const fillColor = heatColor(coalesced.count, 0.18);\n const borderColor = heatColor(coalesced.count, 0.75);\n const labelBackground = heatColor(coalesced.count, 0.9);\n\n const style = element.style;\n style.top = `${rect.top}px`;\n style.left = `${rect.left}px`;\n style.width = `${rect.width}px`;\n style.height = `${rect.height}px`;\n style.backgroundColor = fillColor;\n style.border = `1.5px solid ${borderColor}`;\n style.setProperty(\"--rdu-opacity\", String(config.opacity));\n style.animation = `${OVERLAY_ANIMATION_NAME} ${config.animationDuration}ms ease-out forwards`;\n\n const label = element.firstElementChild as HTMLElement;\n if (config.showLabels) {\n const countText = coalesced.count > 1 ? ` ×${coalesced.count}` : \"\";\n const durationText =\n coalesced.totalDuration > 0\n ? ` ${coalesced.totalDuration.toFixed(1)}ms`\n : \"\";\n const causeText = coalesced.causeSummary\n ? ` (${coalesced.causeSummary})`\n : \"\";\n label.textContent = `${coalesced.component}${countText}${durationText}${causeText}`;\n label.style.backgroundColor = labelBackground;\n } else {\n label.textContent = \"\";\n label.style.backgroundColor = \"transparent\";\n }\n }\n }\n\n function push(entry: HighlightEntry) {\n pending.push(entry);\n if (!timer) {\n timer = setInterval(flush, config.flushInterval);\n }\n }\n\n function dispose() {\n if (timer) {\n clearInterval(timer);\n timer = null;\n }\n pending = [];\n }\n\n return { push, dispose };\n}\n","import type {\n DevToolsHook,\n FiberRoot,\n HighlightEntry,\n MonitorOptions,\n} from \"./types.js\";\nimport {\n detectRenders,\n findNearestDOMNode,\n getComponentName,\n getFiberPath,\n} from \"./fiber.js\";\nimport { detectCauses } from \"./causes.js\";\nimport { logRerendersToConsole } from \"./format.js\";\nimport { createHighlighter } from \"./highlights.js\";\nimport { disposeAllOverlays } from \"./overlay.js\";\n\n/**\n * Ensure __REACT_DEVTOOLS_GLOBAL_HOOK__ exists on window.\n *\n * React reads this hook during initialization — if it's missing, React will\n * never connect. When this library is imported before React (the recommended\n * setup), we need to create a minimal hook so React can find and register with it.\n *\n * If a hook already exists (e.g. from the React DevTools extension), we leave\n * it untouched and hook into it.\n */\nfunction ensureDevToolsHook(win: Window): DevToolsHook {\n const global = win as unknown as {\n __REACT_DEVTOOLS_GLOBAL_HOOK__?: DevToolsHook;\n };\n\n if (!global.__REACT_DEVTOOLS_GLOBAL_HOOK__) {\n global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {\n supportsFiber: true,\n inject() {\n return 1;\n },\n onCommitFiberRoot() {},\n onCommitFiberUnmount() {},\n onPostCommitFiberRoot() {},\n checkDCE() {},\n } as DevToolsHook;\n }\n\n return global.__REACT_DEVTOOLS_GLOBAL_HOOK__;\n}\n\n/**\n * Start monitoring React re-renders.\n *\n * Hooks into `__REACT_DEVTOOLS_GLOBAL_HOOK__.onCommitFiberRoot` to intercept\n * every React commit. Shows visual highlight overlays on re-rendered DOM nodes\n * and optionally logs re-renders to the console.\n *\n * Call this **before** React renders anything — ideally at the very top of\n * your entry point.\n *\n * Returns a `stop` function to unhook and clean up, or `null` if called\n * in a non-browser environment (e.g. SSR).\n */\nexport function startReactUpdatesMonitor({\n logToConsole = false,\n highlight = true,\n mode = \"self-triggered\",\n reasonOfUpdate = false,\n highlightFlushInterval = 250,\n highlightAnimationDuration = 1200,\n highlightShowLabels = true,\n highlightOpacity = 0.3,\n}: MonitorOptions = {}): (() => void) | null {\n // SSR guard — nothing to do without a DOM\n if (typeof window === \"undefined\") return null;\n\n const hook = ensureDevToolsHook(window);\n\n const highlighter = highlight\n ? createHighlighter({\n flushInterval: highlightFlushInterval,\n animationDuration: highlightAnimationDuration,\n showLabels: highlightShowLabels,\n opacity: highlightOpacity,\n })\n : null;\n\n const previousOnCommit = hook.onCommitFiberRoot.bind(hook);\n\n hook.onCommitFiberRoot = (\n rendererID: number,\n root: FiberRoot,\n priorityLevel?: unknown,\n ) => {\n previousOnCommit(rendererID, root, priorityLevel);\n\n // 1. Detect which component fibers actually re-rendered (pure fiber analysis)\n const detectedRenders = detectRenders(root.current, mode);\n if (detectedRenders.length === 0) return;\n\n // 2. Build full entries: resolve names, DOM nodes, causes\n const highlightEntries: HighlightEntry[] = [];\n\n for (let i = 0; i < detectedRenders.length; i++) {\n const { fiber, depth } = detectedRenders[i];\n const name = getComponentName(fiber);\n if (!name) continue;\n\n const entry: HighlightEntry = {\n component: name,\n path: getFiberPath(fiber),\n duration: fiber.actualDuration ?? 0,\n depth,\n domNode: findNearestDOMNode(fiber),\n causes: reasonOfUpdate ? detectCauses(fiber) : [],\n };\n\n highlightEntries.push(entry);\n highlighter?.push(entry);\n }\n\n // 3. Console output\n if (logToConsole) {\n logRerendersToConsole(highlightEntries, reasonOfUpdate);\n }\n };\n\n if (logToConsole) {\n console.log(\n \"%c⚛ react-debug-updates attached\",\n \"color: #61dafb; font-weight: bold\",\n );\n }\n\n return () => {\n hook.onCommitFiberRoot = previousOnCommit;\n highlighter?.dispose();\n disposeAllOverlays();\n };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -320,13 +320,7 @@ function heatColor(count, alpha) {
|
|
|
320
320
|
const lightness = 55 - normalizedCount * 10;
|
|
321
321
|
return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`;
|
|
322
322
|
}
|
|
323
|
-
|
|
324
|
-
flushInterval: 250,
|
|
325
|
-
animationDuration: 1200,
|
|
326
|
-
showLabels: true,
|
|
327
|
-
opacity: 0.3
|
|
328
|
-
};
|
|
329
|
-
function createBatcher(options) {
|
|
323
|
+
function createHighlighter(config) {
|
|
330
324
|
let pending = [];
|
|
331
325
|
let timer = null;
|
|
332
326
|
function flush() {
|
|
@@ -379,10 +373,10 @@ function createBatcher(options) {
|
|
|
379
373
|
style.height = `${rect.height}px`;
|
|
380
374
|
style.backgroundColor = fillColor;
|
|
381
375
|
style.border = `1.5px solid ${borderColor}`;
|
|
382
|
-
style.setProperty("--rdu-opacity", String(
|
|
383
|
-
style.animation = `${OVERLAY_ANIMATION_NAME} ${
|
|
376
|
+
style.setProperty("--rdu-opacity", String(config.opacity));
|
|
377
|
+
style.animation = `${OVERLAY_ANIMATION_NAME} ${config.animationDuration}ms ease-out forwards`;
|
|
384
378
|
const label = element.firstElementChild;
|
|
385
|
-
if (
|
|
379
|
+
if (config.showLabels) {
|
|
386
380
|
const countText = coalesced.count > 1 ? ` ×${coalesced.count}` : "";
|
|
387
381
|
const durationText = coalesced.totalDuration > 0 ? ` ${coalesced.totalDuration.toFixed(1)}ms` : "";
|
|
388
382
|
const causeText = coalesced.causeSummary ? ` (${coalesced.causeSummary})` : "";
|
|
@@ -397,7 +391,7 @@ function createBatcher(options) {
|
|
|
397
391
|
function push(entry) {
|
|
398
392
|
pending.push(entry);
|
|
399
393
|
if (!timer) {
|
|
400
|
-
timer = setInterval(flush,
|
|
394
|
+
timer = setInterval(flush, config.flushInterval);
|
|
401
395
|
}
|
|
402
396
|
}
|
|
403
397
|
function dispose() {
|
|
@@ -409,77 +403,82 @@ function createBatcher(options) {
|
|
|
409
403
|
}
|
|
410
404
|
return { push, dispose };
|
|
411
405
|
}
|
|
412
|
-
function
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
406
|
+
function ensureDevToolsHook(win) {
|
|
407
|
+
const global = win;
|
|
408
|
+
if (!global.__REACT_DEVTOOLS_GLOBAL_HOOK__) {
|
|
409
|
+
global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
|
|
410
|
+
supportsFiber: true,
|
|
411
|
+
inject() {
|
|
412
|
+
return 1;
|
|
413
|
+
},
|
|
414
|
+
onCommitFiberRoot() {
|
|
415
|
+
},
|
|
416
|
+
onCommitFiberUnmount() {
|
|
417
|
+
},
|
|
418
|
+
onPostCommitFiberRoot() {
|
|
419
|
+
},
|
|
420
|
+
checkDCE() {
|
|
421
|
+
}
|
|
422
|
+
};
|
|
427
423
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
424
|
+
return global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
425
|
+
}
|
|
426
|
+
function startReactUpdatesMonitor({
|
|
427
|
+
logToConsole = false,
|
|
428
|
+
highlight = true,
|
|
429
|
+
mode = "self-triggered",
|
|
430
|
+
reasonOfUpdate = false,
|
|
431
|
+
highlightFlushInterval = 250,
|
|
432
|
+
highlightAnimationDuration = 1200,
|
|
433
|
+
highlightShowLabels = true,
|
|
434
|
+
highlightOpacity = 0.3
|
|
435
|
+
} = {}) {
|
|
436
|
+
if (typeof window === "undefined") return null;
|
|
437
|
+
const hook = ensureDevToolsHook(window);
|
|
438
|
+
const highlighter = highlight ? createHighlighter({
|
|
439
|
+
flushInterval: highlightFlushInterval,
|
|
440
|
+
animationDuration: highlightAnimationDuration,
|
|
441
|
+
showLabels: highlightShowLabels,
|
|
442
|
+
opacity: highlightOpacity
|
|
443
|
+
}) : null;
|
|
434
444
|
const previousOnCommit = hook.onCommitFiberRoot.bind(hook);
|
|
435
445
|
hook.onCommitFiberRoot = (rendererID, root, priorityLevel) => {
|
|
436
446
|
previousOnCommit(rendererID, root, priorityLevel);
|
|
437
447
|
const detectedRenders = detectRenders(root.current, mode);
|
|
438
448
|
if (detectedRenders.length === 0) return;
|
|
439
|
-
const
|
|
449
|
+
const highlightEntries = [];
|
|
440
450
|
for (let i = 0; i < detectedRenders.length; i++) {
|
|
441
451
|
const { fiber, depth } = detectedRenders[i];
|
|
442
452
|
const name = getComponentName(fiber);
|
|
443
453
|
if (!name) continue;
|
|
444
|
-
const
|
|
454
|
+
const entry = {
|
|
445
455
|
component: name,
|
|
446
456
|
path: getFiberPath(fiber),
|
|
447
457
|
duration: fiber.actualDuration ?? 0,
|
|
448
458
|
depth,
|
|
449
459
|
domNode: findNearestDOMNode(fiber),
|
|
450
|
-
causes:
|
|
451
|
-
};
|
|
452
|
-
const renderEntry = {
|
|
453
|
-
component: pendingEntry.component,
|
|
454
|
-
path: pendingEntry.path,
|
|
455
|
-
duration: pendingEntry.duration,
|
|
456
|
-
timestamp: performance.now(),
|
|
457
|
-
causes: pendingEntry.causes
|
|
460
|
+
causes: reasonOfUpdate ? detectCauses(fiber) : []
|
|
458
461
|
};
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
entries.push(renderEntry);
|
|
462
|
-
pendingEntries.push(pendingEntry);
|
|
463
|
-
batcher == null ? void 0 : batcher.push(pendingEntry);
|
|
462
|
+
highlightEntries.push(entry);
|
|
463
|
+
highlighter == null ? void 0 : highlighter.push(entry);
|
|
464
464
|
}
|
|
465
|
-
if (
|
|
466
|
-
logRerendersToConsole(
|
|
465
|
+
if (logToConsole) {
|
|
466
|
+
logRerendersToConsole(highlightEntries, reasonOfUpdate);
|
|
467
467
|
}
|
|
468
468
|
};
|
|
469
|
-
|
|
470
|
-
hook.onCommitFiberRoot = previousOnCommit;
|
|
471
|
-
batcher == null ? void 0 : batcher.dispose();
|
|
472
|
-
disposeAllOverlays();
|
|
473
|
-
};
|
|
474
|
-
if (!silent) {
|
|
469
|
+
if (logToConsole) {
|
|
475
470
|
console.log(
|
|
476
471
|
"%c⚛ react-debug-updates attached",
|
|
477
472
|
"color: #61dafb; font-weight: bold"
|
|
478
473
|
);
|
|
479
474
|
}
|
|
480
|
-
return
|
|
475
|
+
return () => {
|
|
476
|
+
hook.onCommitFiberRoot = previousOnCommit;
|
|
477
|
+
highlighter == null ? void 0 : highlighter.dispose();
|
|
478
|
+
disposeAllOverlays();
|
|
479
|
+
};
|
|
481
480
|
}
|
|
482
481
|
export {
|
|
483
|
-
|
|
482
|
+
startReactUpdatesMonitor
|
|
484
483
|
};
|
|
485
484
|
//# sourceMappingURL=react-debug-updates.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react-debug-updates.js","sources":["../src/fiber.ts","../src/causes.ts","../src/format.ts","../src/overlay.ts","../src/batcher.ts","../src/logger.ts"],"sourcesContent":["import type { Fiber, DetectedRender } from \"./types.js\";\n\n// ────────────────────────────────────────────\n// Fiber tag constants\n// ────────────────────────────────────────────\n\nexport const FiberTag = {\n FunctionComponent: 0,\n ClassComponent: 1,\n HostComponent: 5,\n ForwardRef: 11,\n SimpleMemoComponent: 15,\n MemoComponent: 14,\n} as const;\n\n// Component tags we report as re-renders.\n// MemoComponent (14) is excluded to avoid double-reporting — it's a wrapper\n// fiber around the inner component. The inner component (FunctionComponent,\n// ForwardRef, or SimpleMemoComponent) has its own PerformedWork flag and\n// will be reported correctly on its own.\nconst COMPONENT_TAGS = new Set<number>([\n FiberTag.FunctionComponent,\n FiberTag.ClassComponent,\n FiberTag.ForwardRef,\n FiberTag.SimpleMemoComponent,\n]);\n\nconst PerformedWork = 0b0000001;\n\n// ────────────────────────────────────────────\n// Fiber tree helpers\n// ────────────────────────────────────────────\n\nexport function getComponentName(fiber: Fiber): string | null {\n const { type } = fiber;\n if (!type || typeof type === \"string\") return null;\n return type.displayName ?? type.name ?? null;\n}\n\nfunction isHTMLElement(value: unknown): value is HTMLElement {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as Node).nodeType === 1 &&\n typeof (value as HTMLElement).getBoundingClientRect === \"function\"\n );\n}\n\nexport function findNearestDOMNode(fiber: Fiber): HTMLElement | null {\n if (fiber.tag === FiberTag.HostComponent && isHTMLElement(fiber.stateNode)) {\n return fiber.stateNode;\n }\n let child: Fiber | null = fiber.child;\n while (child) {\n const found = findNearestDOMNode(child);\n if (found) return found;\n child = child.sibling;\n }\n return null;\n}\n\nexport function getFiberPath(fiber: Fiber, maxDepth = 5): string {\n const parts: string[] = [];\n let current: Fiber | null = fiber.return;\n let depth = 0;\n while (current && depth < maxDepth) {\n const name = getComponentName(current);\n if (name) parts.unshift(name);\n current = current.return;\n depth++;\n }\n return parts.length ? parts.join(\" → \") : \"(root)\";\n}\n\n// ────────────────────────────────────────────\n// Self-triggered detection\n// ────────────────────────────────────────────\n\nfunction isSelfTriggered(fiber: Fiber): boolean {\n const alternate = fiber.alternate;\n if (!alternate) return false;\n return fiber.memoizedProps === alternate.memoizedProps;\n}\n\n// ────────────────────────────────────────────\n// didFiberRender — mirrors React DevTools\n// ────────────────────────────────────────────\n//\n// See: react-devtools-shared/src/backend/fiber/shared/DevToolsFiberChangeDetection.js\n//\n// For component fibers (function, class, memo, forwardRef), React sets the\n// PerformedWork flag (bit 0) only when user code actually executes.\n// createWorkInProgress resets flags to NoFlags, so PerformedWork on a\n// work-in-progress fiber is always fresh — never stale from a prior commit.\n//\n// This check must only be called AFTER confirming prevFiber !== nextFiber\n// (i.e. the fiber was actually processed, not a bailed-out subtree).\n\nfunction didFiberRender(nextFiber: Fiber): boolean {\n return (nextFiber.flags & PerformedWork) === PerformedWork;\n}\n\n// ────────────────────────────────────────────\n// Detect which components re-rendered in a commit\n// ────────────────────────────────────────────\n//\n// Mirrors React DevTools' updateFiberRecursively / updateChildrenRecursively.\n//\n// React double-buffers fibers. After a commit, root.current is the committed\n// tree and root.current.alternate is the previous tree. We walk both in\n// parallel. At each level, if prevChild and nextChild are the same object,\n// React bailed out that entire subtree (via cloneChildFibers) — we skip it.\n// Otherwise, we use nextChild.alternate as the previous fiber and check\n// didFiberRender (PerformedWork) to see if user code actually ran.\n//\n// Returns lightweight DetectedRender objects — just the fiber and its depth.\n// DOM lookup, cause detection, and formatting are the caller's responsibility.\n\nexport function detectRenders(\n root: Fiber,\n mode: \"self-triggered\" | \"all\",\n): DetectedRender[] {\n const results: DetectedRender[] = [];\n const selfTriggeredOnly = mode === \"self-triggered\";\n\n // The alternate of the committed root is the previous tree's root.\n // On initial mount this is null — nothing to report.\n const previousRoot = root.alternate;\n if (!previousRoot) return results;\n\n function walk(\n nextFiber: Fiber,\n previousFiber: Fiber | null,\n depth: number,\n ) {\n // ── Check this fiber ──\n if (\n COMPONENT_TAGS.has(nextFiber.tag) &&\n previousFiber !== null &&\n previousFiber !== nextFiber && // same object → bailed-out subtree\n didFiberRender(nextFiber) &&\n (!selfTriggeredOnly || isSelfTriggered(nextFiber))\n ) {\n results.push({ fiber: nextFiber, depth });\n }\n\n // ── Walk children, matching with previous tree ──\n let nextChild = nextFiber.child;\n let previousChildAtSameIndex = previousFiber?.child ?? null;\n\n while (nextChild) {\n let matchedPrevious: Fiber | null;\n\n if (previousChildAtSameIndex === nextChild) {\n // Same object identity — React shared this fiber via cloneChildFibers.\n // The entire subtree was bailed out; passing the same object as both\n // prev and next causes the prevFiber !== nextFiber guard to skip it.\n matchedPrevious = nextChild;\n } else {\n // Different object — this fiber was processed. The alternate is the\n // corresponding fiber from the previous tree.\n matchedPrevious = nextChild.alternate;\n }\n\n walk(nextChild, matchedPrevious, depth + 1);\n\n nextChild = nextChild.sibling;\n previousChildAtSameIndex = previousChildAtSameIndex?.sibling ?? null;\n }\n }\n\n walk(root, previousRoot, 0);\n return results;\n}\n","import type { Fiber, HookNode, UpdateCause } from \"./types.js\";\nimport { FiberTag } from \"./fiber.js\";\n\n// ────────────────────────────────────────────\n// Update cause detection\n// ────────────────────────────────────────────\n//\n// For function components, React stores hooks as a linked list on\n// fiber.memoizedState. In dev builds, _debugHookTypes lists hook names\n// in call order. We walk both in parallel, comparing memoizedState\n// on each node to find which hook's state actually changed.\n//\n// Caveat: useContext does NOT create a linked list node, so we skip it\n// when advancing the pointer, and detect it by elimination.\n\n/**\n * Hooks whose memoizedState changing can trigger a re-render.\n * We only report these — useEffect/useMemo/etc. don't cause re-renders.\n */\nconst STATE_HOOKS = new Set([\n \"useState\",\n \"useReducer\",\n \"useSyncExternalStore\",\n]);\n\n/**\n * Hooks that do NOT create a node in the memoizedState linked list.\n * Currently only useContext — every other hook allocates a node.\n */\nconst HOOKS_WITHOUT_NODE = new Set([\"useContext\"]);\n\nexport function detectCauses(fiber: Fiber): UpdateCause[] {\n const alternate = fiber.alternate;\n if (!alternate) return [];\n\n const causes: UpdateCause[] = [];\n\n // ── Props changed (parent re-rendered us with new props) ──\n if (fiber.memoizedProps !== alternate.memoizedProps) {\n causes.push({ kind: \"props\" });\n }\n\n // ── Class component ──\n if (fiber.tag === FiberTag.ClassComponent) {\n if (fiber.memoizedState !== alternate.memoizedState) {\n causes.push({ kind: \"class-state\" });\n }\n return causes;\n }\n\n // ── Function component hooks ──\n const hookTypes = fiber._debugHookTypes;\n\n if (!hookTypes) {\n // No debug info (prod build) — best effort\n if (fiber.memoizedState !== alternate.memoizedState) {\n causes.push({ kind: \"unknown\" });\n }\n return causes;\n }\n\n let currentNode = fiber.memoizedState as HookNode | null;\n let previousNode = alternate.memoizedState as HookNode | null;\n let hasContextHook = false;\n let anyStateHookChanged = false;\n\n for (let i = 0; i < hookTypes.length; i++) {\n const type = hookTypes[i];\n\n // useContext has no linked list node — skip pointer advance\n if (HOOKS_WITHOUT_NODE.has(type)) {\n if (type === \"useContext\") hasContextHook = true;\n continue;\n }\n\n if (STATE_HOOKS.has(type) && currentNode && previousNode) {\n if (!Object.is(currentNode.memoizedState, previousNode.memoizedState)) {\n anyStateHookChanged = true;\n causes.push({\n kind: \"hook\",\n hookIndex: i,\n hookType: type,\n previousValue: previousNode.memoizedState,\n nextValue: currentNode.memoizedState,\n });\n }\n }\n\n currentNode = currentNode?.next ?? null;\n previousNode = previousNode?.next ?? null;\n }\n\n // If no state hook changed but component is self-triggered and has\n // useContext → the context value must have changed.\n if (\n hasContextHook &&\n !anyStateHookChanged &&\n fiber.memoizedProps === alternate.memoizedProps\n ) {\n causes.push({ kind: \"hook\", hookType: \"useContext\" });\n }\n\n // If still nothing and self-triggered, mark unknown\n if (causes.length === 0 && fiber.memoizedProps === alternate.memoizedProps) {\n causes.push({ kind: \"unknown\" });\n }\n\n return causes;\n}\n","import type { PendingEntry, UpdateCause } from \"./types.js\";\n\n// ────────────────────────────────────────────\n// Value formatting\n// ────────────────────────────────────────────\n\nexport function formatValue(value: unknown, maxLength = 50): string {\n if (value === null) return \"null\";\n if (value === undefined) return \"undefined\";\n if (typeof value === \"boolean\") return String(value);\n if (typeof value === \"number\") return String(value);\n if (typeof value === \"function\")\n return `ƒ ${(value as { name?: string }).name || \"anonymous\"}`;\n\n if (typeof value === \"string\") {\n const truncated =\n value.length > maxLength ? value.slice(0, maxLength) + \"…\" : value;\n return JSON.stringify(truncated);\n }\n\n if (Array.isArray(value)) return `Array(${value.length})`;\n\n if (typeof value === \"object\") {\n const name = (value as object).constructor?.name;\n if (name && name !== \"Object\") return name;\n const keys = Object.keys(value as object);\n if (keys.length <= 3) return `{ ${keys.join(\", \")} }`;\n return `{ ${keys.slice(0, 3).join(\", \")}, … }`;\n }\n\n return String(value);\n}\n\n// ────────────────────────────────────────────\n// Cause formatting\n// ────────────────────────────────────────────\n\n/** Compact summary for overlay labels. */\nexport function formatCausesShort(causes: UpdateCause[]): string {\n if (causes.length === 0) return \"\";\n\n const parts: string[] = [];\n for (const cause of causes) {\n if (cause.kind === \"props\") {\n parts.push(\"props\");\n } else if (cause.kind === \"class-state\") {\n parts.push(\"state\");\n } else if (cause.kind === \"hook\" && cause.hookType) {\n const indexSuffix = cause.hookIndex != null ? `[${cause.hookIndex}]` : \"\";\n parts.push(`${cause.hookType}${indexSuffix}`);\n } else {\n parts.push(\"?\");\n }\n }\n\n return parts.join(\", \");\n}\n\n/** Detailed lines for console output. */\nexport function formatCausesConsole(causes: UpdateCause[]): string[] {\n const lines: string[] = [];\n\n for (const cause of causes) {\n if (cause.kind === \"props\") {\n lines.push(\" ↳ props changed (parent re-rendered)\");\n } else if (cause.kind === \"class-state\") {\n lines.push(\" ↳ class state changed\");\n } else if (cause.kind === \"hook\" && cause.hookType) {\n const indexSuffix =\n cause.hookIndex != null ? `[${cause.hookIndex}]` : \"\";\n const name = `${cause.hookType}${indexSuffix}`;\n\n if (cause.previousValue !== undefined && cause.nextValue !== undefined) {\n lines.push(\n ` ↳ ${name}: ${formatValue(cause.previousValue)} → ${formatValue(cause.nextValue)}`,\n );\n } else {\n lines.push(` ↳ ${name} changed`);\n }\n } else {\n lines.push(\" ↳ unknown cause\");\n }\n }\n\n return lines;\n}\n\n// ────────────────────────────────────────────\n// Console output\n// ────────────────────────────────────────────\n\n/** Log re-renders as a collapsed console group. */\nexport function logRerendersToConsole(\n entries: PendingEntry[],\n showCauses: boolean,\n) {\n if (entries.length === 0) return;\n\n console.groupCollapsed(\n `%c⚛ ${entries.length} re-render${entries.length > 1 ? \"s\" : \"\"}`,\n \"color: #61dafb; font-weight: bold\",\n );\n\n for (let i = 0; i < entries.length; i++) {\n const entry = entries[i];\n const durationText =\n entry.duration > 0 ? ` (${entry.duration.toFixed(2)}ms)` : \"\";\n console.log(\n `%c${entry.component}%c ${entry.path}${durationText}`,\n \"color: #e8e82e; font-weight: bold\",\n \"color: #888\",\n );\n\n if (showCauses && entry.causes.length > 0) {\n const lines = formatCausesConsole(entry.causes);\n for (const line of lines) {\n console.log(`%c${line}`, \"color: #aaa\");\n }\n }\n }\n\n console.groupEnd();\n}\n","// ────────────────────────────────────────────\n// Per-window overlay infrastructure\n// ────────────────────────────────────────────\n//\n// Performance:\n// - Overlay elements pooled per window, reused via animationend\n// - Single CSS @keyframes per window, no JS timers per element\n\nconst ANIMATION_NAME = \"__rdu-fade\";\n\nconst injectedWindows = new WeakSet<Window>();\n\nfunction ensureStylesheet(win: Window) {\n if (injectedWindows.has(win)) return;\n injectedWindows.add(win);\n\n const style = win.document.createElement(\"style\");\n style.textContent = `\n @keyframes ${ANIMATION_NAME} {\n 0% { opacity: 0; }\n 8% { opacity: var(--rdu-opacity, 1); }\n 40% { opacity: var(--rdu-opacity, 1); }\n 100% { opacity: 0; }\n }\n .${ANIMATION_NAME}-box {\n position: fixed;\n pointer-events: none;\n box-sizing: border-box;\n border-radius: 3px;\n opacity: 0;\n will-change: opacity;\n }\n .${ANIMATION_NAME}-label {\n position: absolute;\n top: -18px;\n left: -1px;\n font: 10px/16px ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace;\n padding: 0 4px;\n color: #fff;\n border-radius: 2px 2px 0 0;\n white-space: nowrap;\n pointer-events: none;\n }\n `;\n win.document.head.appendChild(style);\n}\n\n// ────────────────────────────────────────────\n// Overlay root container\n// ────────────────────────────────────────────\n\nconst overlayRoots = new WeakMap<Window, HTMLDivElement>();\n\nfunction getOverlayRoot(win: Window): HTMLDivElement | null {\n if (win.closed) return null;\n\n const existing = overlayRoots.get(win);\n if (existing?.isConnected) return existing;\n\n ensureStylesheet(win);\n\n const root = win.document.createElement(\"div\");\n root.id = \"__react-debug-updates\";\n Object.assign(root.style, {\n position: \"fixed\",\n top: \"0\",\n left: \"0\",\n width: \"0\",\n height: \"0\",\n overflow: \"visible\",\n pointerEvents: \"none\",\n zIndex: \"2147483647\",\n } satisfies Partial<CSSStyleDeclaration>);\n win.document.body.appendChild(root);\n\n overlayRoots.set(win, root);\n return root;\n}\n\n// ────────────────────────────────────────────\n// Element pool\n// ────────────────────────────────────────────\n\nconst pools = new WeakMap<Window, HTMLDivElement[]>();\n\nexport function acquireOverlay(win: Window): HTMLDivElement | null {\n const root = getOverlayRoot(win);\n if (!root) return null;\n\n let pool = pools.get(win);\n if (!pool) {\n pool = [];\n pools.set(win, pool);\n }\n\n const document = win.document;\n let element = pool.pop();\n\n if (!element) {\n element = document.createElement(\"div\");\n element.className = `${ANIMATION_NAME}-box`;\n\n const label = document.createElement(\"span\");\n label.className = `${ANIMATION_NAME}-label`;\n element.appendChild(label);\n\n element.addEventListener(\"animationend\", () => {\n element!.style.animation = \"none\";\n element!.remove();\n pool!.push(element!);\n });\n }\n\n root.appendChild(element);\n return element;\n}\n\nexport function disposeAllOverlays() {\n const mainRoot = overlayRoots.get(window);\n mainRoot?.remove();\n overlayRoots.delete(window);\n pools.delete(window);\n}\n\nexport const OVERLAY_ANIMATION_NAME = ANIMATION_NAME;\n","import type { HighlightOptions, PendingEntry } from \"./types.js\";\nimport { formatCausesShort } from \"./format.js\";\nimport { acquireOverlay, OVERLAY_ANIMATION_NAME } from \"./overlay.js\";\n\n// ────────────────────────────────────────────\n// Heat color\n// ────────────────────────────────────────────\n\nfunction heatColor(count: number, alpha: number): string {\n const normalizedCount = Math.min((count - 1) / 7, 1);\n const hue = 200 - normalizedCount * 200;\n const saturation = 85 + normalizedCount * 15;\n const lightness = 55 - normalizedCount * 10;\n return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`;\n}\n\n// ────────────────────────────────────────────\n// Batched flush\n// ────────────────────────────────────────────\n//\n// Commit path just pushes lightweight refs (no DOM reads, no formatting).\n// Flush (setInterval): batched rect reads → batched DOM writes.\n\nexport const HIGHLIGHT_DEFAULTS: Required<HighlightOptions> = {\n flushInterval: 250,\n animationDuration: 1200,\n showLabels: true,\n opacity: 0.3,\n};\n\ninterface CoalescedEntry {\n count: number;\n totalDuration: number;\n component: string;\n /** Shallowest fiber depth among coalesced entries (for z-ordering). */\n depth: number;\n domNode: HTMLElement;\n ownerWindow: Window;\n causeSummary: string;\n}\n\nexport function createBatcher(options: Required<HighlightOptions>) {\n let pending: PendingEntry[] = [];\n let timer: ReturnType<typeof setInterval> | null = null;\n\n function flush() {\n if (pending.length === 0) return;\n\n const batch = pending;\n pending = [];\n\n // Coalesce by DOM node identity\n const map = new Map<HTMLElement, CoalescedEntry>();\n\n for (let i = 0; i < batch.length; i++) {\n const entry = batch[i];\n if (!entry.domNode) continue;\n\n const existing = map.get(entry.domNode);\n if (existing) {\n existing.count++;\n existing.totalDuration += entry.duration;\n existing.depth = Math.min(existing.depth, entry.depth);\n } else {\n const win = entry.domNode.ownerDocument?.defaultView;\n if (!win || win.closed) continue;\n map.set(entry.domNode, {\n count: 1,\n totalDuration: entry.duration,\n component: entry.component,\n depth: entry.depth,\n domNode: entry.domNode,\n ownerWindow: win,\n causeSummary: formatCausesShort(entry.causes),\n });\n }\n }\n\n // Read phase: batch all rect reads\n const toShow: Array<{ coalesced: CoalescedEntry; rect: DOMRect }> = [];\n for (const coalesced of map.values()) {\n const rect = coalesced.domNode.getBoundingClientRect();\n if (rect.width > 0 || rect.height > 0) {\n toShow.push({ coalesced, rect });\n }\n }\n\n // Sort deepest first so parents are appended last (render on top)\n toShow.sort((a, b) => b.coalesced.depth - a.coalesced.depth);\n\n // Write phase: position overlays\n for (let i = 0; i < toShow.length; i++) {\n const { coalesced, rect } = toShow[i];\n const element = acquireOverlay(coalesced.ownerWindow);\n if (!element) continue;\n\n const fillColor = heatColor(coalesced.count, 0.18);\n const borderColor = heatColor(coalesced.count, 0.75);\n const labelBackground = heatColor(coalesced.count, 0.9);\n\n const style = element.style;\n style.top = `${rect.top}px`;\n style.left = `${rect.left}px`;\n style.width = `${rect.width}px`;\n style.height = `${rect.height}px`;\n style.backgroundColor = fillColor;\n style.border = `1.5px solid ${borderColor}`;\n style.setProperty(\"--rdu-opacity\", String(options.opacity));\n style.animation = `${OVERLAY_ANIMATION_NAME} ${options.animationDuration}ms ease-out forwards`;\n\n const label = element.firstElementChild as HTMLElement;\n if (options.showLabels) {\n const countText = coalesced.count > 1 ? ` ×${coalesced.count}` : \"\";\n const durationText =\n coalesced.totalDuration > 0\n ? ` ${coalesced.totalDuration.toFixed(1)}ms`\n : \"\";\n const causeText = coalesced.causeSummary\n ? ` (${coalesced.causeSummary})`\n : \"\";\n label.textContent = `${coalesced.component}${countText}${durationText}${causeText}`;\n label.style.backgroundColor = labelBackground;\n } else {\n label.textContent = \"\";\n label.style.backgroundColor = \"transparent\";\n }\n }\n }\n\n function push(entry: PendingEntry) {\n pending.push(entry);\n if (!timer) {\n timer = setInterval(flush, options.flushInterval);\n }\n }\n\n function dispose() {\n if (timer) {\n clearInterval(timer);\n timer = null;\n }\n pending = [];\n }\n\n return { push, dispose };\n}\n","import type {\n DevToolsHook,\n FiberRoot,\n LoggerOptions,\n PendingEntry,\n RenderEntry,\n RenderLogger,\n} from \"./types.js\";\nimport {\n detectRenders,\n findNearestDOMNode,\n getComponentName,\n getFiberPath,\n} from \"./fiber.js\";\nimport { detectCauses } from \"./causes.js\";\nimport { logRerendersToConsole } from \"./format.js\";\nimport { HIGHLIGHT_DEFAULTS, createBatcher } from \"./batcher.js\";\nimport { disposeAllOverlays } from \"./overlay.js\";\n\n/**\n * Attach a render logger to React's DevTools hook.\n *\n * Hooks into `__REACT_DEVTOOLS_GLOBAL_HOOK__.onCommitFiberRoot` to intercept\n * every React commit. Records re-render entries, optionally logs them to the\n * console, and optionally shows visual highlight overlays on re-rendered DOM nodes.\n *\n * Returns a `RenderLogger` handle, or `null` if the DevTools hook is not found.\n */\nexport function attachRenderLogger(\n options: LoggerOptions = {},\n): RenderLogger | null {\n const {\n silent = false,\n bufferSize = 500,\n filter,\n highlight = false,\n mode = \"self-triggered\",\n showCauses = false,\n } = options;\n\n const hook = (\n window as unknown as { __REACT_DEVTOOLS_GLOBAL_HOOK__?: DevToolsHook }\n ).__REACT_DEVTOOLS_GLOBAL_HOOK__;\n\n if (!hook) {\n console.warn(\n \"[react-debug-updates] __REACT_DEVTOOLS_GLOBAL_HOOK__ not found. \" +\n \"Make sure React DevTools or a dev build of React is active.\",\n );\n return null;\n }\n\n const highlightOptions = highlight\n ? {\n ...HIGHLIGHT_DEFAULTS,\n ...(typeof highlight === \"object\" ? highlight : {}),\n }\n : null;\n\n const batcher = highlightOptions ? createBatcher(highlightOptions) : null;\n\n const entries: RenderEntry[] = [];\n const previousOnCommit = hook.onCommitFiberRoot.bind(hook);\n\n hook.onCommitFiberRoot = (\n rendererID: number,\n root: FiberRoot,\n priorityLevel?: unknown,\n ) => {\n previousOnCommit(rendererID, root, priorityLevel);\n\n // 1. Detect which component fibers actually re-rendered (pure fiber analysis)\n const detectedRenders = detectRenders(root.current, mode);\n if (detectedRenders.length === 0) return;\n\n // 2. Build full entries: resolve names, DOM nodes, causes\n const pendingEntries: PendingEntry[] = [];\n\n for (let i = 0; i < detectedRenders.length; i++) {\n const { fiber, depth } = detectedRenders[i];\n const name = getComponentName(fiber);\n if (!name) continue;\n\n const pendingEntry: PendingEntry = {\n component: name,\n path: getFiberPath(fiber),\n duration: fiber.actualDuration ?? 0,\n depth,\n domNode: findNearestDOMNode(fiber),\n causes: showCauses ? detectCauses(fiber) : [],\n };\n\n const renderEntry: RenderEntry = {\n component: pendingEntry.component,\n path: pendingEntry.path,\n duration: pendingEntry.duration,\n timestamp: performance.now(),\n causes: pendingEntry.causes,\n };\n\n if (filter && !filter(renderEntry)) continue;\n\n if (entries.length >= bufferSize) entries.shift();\n entries.push(renderEntry);\n\n pendingEntries.push(pendingEntry);\n batcher?.push(pendingEntry);\n }\n\n // 3. Console output\n if (!silent) {\n logRerendersToConsole(pendingEntries, showCauses);\n }\n };\n\n const disconnect = () => {\n hook.onCommitFiberRoot = previousOnCommit;\n batcher?.dispose();\n disposeAllOverlays();\n };\n\n if (!silent) {\n console.log(\n \"%c⚛ react-debug-updates attached\",\n \"color: #61dafb; font-weight: bold\",\n );\n }\n\n return { entries, disconnect };\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"react-debug-updates.js","sources":["../src/fiber.ts","../src/causes.ts","../src/format.ts","../src/overlay.ts","../src/highlights.ts","../src/monitor.ts"],"sourcesContent":["import type { Fiber, DetectedRender } from \"./types.js\";\n\n// ────────────────────────────────────────────\n// Fiber tag constants\n// ────────────────────────────────────────────\n\nexport const FiberTag = {\n FunctionComponent: 0,\n ClassComponent: 1,\n HostComponent: 5,\n ForwardRef: 11,\n SimpleMemoComponent: 15,\n MemoComponent: 14,\n} as const;\n\n// Component tags we report as re-renders.\n// MemoComponent (14) is excluded to avoid double-reporting — it's a wrapper\n// fiber around the inner component. The inner component (FunctionComponent,\n// ForwardRef, or SimpleMemoComponent) has its own PerformedWork flag and\n// will be reported correctly on its own.\nconst COMPONENT_TAGS = new Set<number>([\n FiberTag.FunctionComponent,\n FiberTag.ClassComponent,\n FiberTag.ForwardRef,\n FiberTag.SimpleMemoComponent,\n]);\n\nconst PerformedWork = 0b0000001;\n\n// ────────────────────────────────────────────\n// Fiber tree helpers\n// ────────────────────────────────────────────\n\nexport function getComponentName(fiber: Fiber): string | null {\n const { type } = fiber;\n if (!type || typeof type === \"string\") return null;\n return type.displayName ?? type.name ?? null;\n}\n\nfunction isHTMLElement(value: unknown): value is HTMLElement {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as Node).nodeType === 1 &&\n typeof (value as HTMLElement).getBoundingClientRect === \"function\"\n );\n}\n\nexport function findNearestDOMNode(fiber: Fiber): HTMLElement | null {\n if (fiber.tag === FiberTag.HostComponent && isHTMLElement(fiber.stateNode)) {\n return fiber.stateNode;\n }\n let child: Fiber | null = fiber.child;\n while (child) {\n const found = findNearestDOMNode(child);\n if (found) return found;\n child = child.sibling;\n }\n return null;\n}\n\nexport function getFiberPath(fiber: Fiber, maxDepth = 5): string {\n const parts: string[] = [];\n let current: Fiber | null = fiber.return;\n let depth = 0;\n while (current && depth < maxDepth) {\n const name = getComponentName(current);\n if (name) parts.unshift(name);\n current = current.return;\n depth++;\n }\n return parts.length ? parts.join(\" → \") : \"(root)\";\n}\n\n// ────────────────────────────────────────────\n// Self-triggered detection\n// ────────────────────────────────────────────\n\nfunction isSelfTriggered(fiber: Fiber): boolean {\n const alternate = fiber.alternate;\n if (!alternate) return false;\n return fiber.memoizedProps === alternate.memoizedProps;\n}\n\n// ────────────────────────────────────────────\n// didFiberRender — mirrors React DevTools\n// ────────────────────────────────────────────\n//\n// See: react-devtools-shared/src/backend/fiber/shared/DevToolsFiberChangeDetection.js\n//\n// For component fibers (function, class, memo, forwardRef), React sets the\n// PerformedWork flag (bit 0) only when user code actually executes.\n// createWorkInProgress resets flags to NoFlags, so PerformedWork on a\n// work-in-progress fiber is always fresh — never stale from a prior commit.\n//\n// This check must only be called AFTER confirming prevFiber !== nextFiber\n// (i.e. the fiber was actually processed, not a bailed-out subtree).\n\nfunction didFiberRender(nextFiber: Fiber): boolean {\n return (nextFiber.flags & PerformedWork) === PerformedWork;\n}\n\n// ────────────────────────────────────────────\n// Detect which components re-rendered in a commit\n// ────────────────────────────────────────────\n//\n// Mirrors React DevTools' updateFiberRecursively / updateChildrenRecursively.\n//\n// React double-buffers fibers. After a commit, root.current is the committed\n// tree and root.current.alternate is the previous tree. We walk both in\n// parallel. At each level, if prevChild and nextChild are the same object,\n// React bailed out that entire subtree (via cloneChildFibers) — we skip it.\n// Otherwise, we use nextChild.alternate as the previous fiber and check\n// didFiberRender (PerformedWork) to see if user code actually ran.\n//\n// Returns lightweight DetectedRender objects — just the fiber and its depth.\n// DOM lookup, cause detection, and formatting are the caller's responsibility.\n\nexport function detectRenders(\n root: Fiber,\n mode: \"self-triggered\" | \"all\",\n): DetectedRender[] {\n const results: DetectedRender[] = [];\n const selfTriggeredOnly = mode === \"self-triggered\";\n\n // The alternate of the committed root is the previous tree's root.\n // On initial mount this is null — nothing to report.\n const previousRoot = root.alternate;\n if (!previousRoot) return results;\n\n function walk(\n nextFiber: Fiber,\n previousFiber: Fiber | null,\n depth: number,\n ) {\n // ── Check this fiber ──\n if (\n COMPONENT_TAGS.has(nextFiber.tag) &&\n previousFiber !== null &&\n previousFiber !== nextFiber && // same object → bailed-out subtree\n didFiberRender(nextFiber) &&\n (!selfTriggeredOnly || isSelfTriggered(nextFiber))\n ) {\n results.push({ fiber: nextFiber, depth });\n }\n\n // ── Walk children, matching with previous tree ──\n let nextChild = nextFiber.child;\n let previousChildAtSameIndex = previousFiber?.child ?? null;\n\n while (nextChild) {\n let matchedPrevious: Fiber | null;\n\n if (previousChildAtSameIndex === nextChild) {\n // Same object identity — React shared this fiber via cloneChildFibers.\n // The entire subtree was bailed out; passing the same object as both\n // prev and next causes the prevFiber !== nextFiber guard to skip it.\n matchedPrevious = nextChild;\n } else {\n // Different object — this fiber was processed. The alternate is the\n // corresponding fiber from the previous tree.\n matchedPrevious = nextChild.alternate;\n }\n\n walk(nextChild, matchedPrevious, depth + 1);\n\n nextChild = nextChild.sibling;\n previousChildAtSameIndex = previousChildAtSameIndex?.sibling ?? null;\n }\n }\n\n walk(root, previousRoot, 0);\n return results;\n}\n","import type { Fiber, HookNode, UpdateCause } from \"./types.js\";\nimport { FiberTag } from \"./fiber.js\";\n\n// ────────────────────────────────────────────\n// Update cause detection\n// ────────────────────────────────────────────\n//\n// For function components, React stores hooks as a linked list on\n// fiber.memoizedState. In dev builds, _debugHookTypes lists hook names\n// in call order. We walk both in parallel, comparing memoizedState\n// on each node to find which hook's state actually changed.\n//\n// Caveat: useContext does NOT create a linked list node, so we skip it\n// when advancing the pointer, and detect it by elimination.\n\n/**\n * Hooks whose memoizedState changing can trigger a re-render.\n * We only report these — useEffect/useMemo/etc. don't cause re-renders.\n */\nconst STATE_HOOKS = new Set([\n \"useState\",\n \"useReducer\",\n \"useSyncExternalStore\",\n]);\n\n/**\n * Hooks that do NOT create a node in the memoizedState linked list.\n * Currently only useContext — every other hook allocates a node.\n */\nconst HOOKS_WITHOUT_NODE = new Set([\"useContext\"]);\n\nexport function detectCauses(fiber: Fiber): UpdateCause[] {\n const alternate = fiber.alternate;\n if (!alternate) return [];\n\n const causes: UpdateCause[] = [];\n\n // ── Props changed (parent re-rendered us with new props) ──\n if (fiber.memoizedProps !== alternate.memoizedProps) {\n causes.push({ kind: \"props\" });\n }\n\n // ── Class component ──\n if (fiber.tag === FiberTag.ClassComponent) {\n if (fiber.memoizedState !== alternate.memoizedState) {\n causes.push({ kind: \"class-state\" });\n }\n return causes;\n }\n\n // ── Function component hooks ──\n const hookTypes = fiber._debugHookTypes;\n\n if (!hookTypes) {\n // No debug info (prod build) — best effort\n if (fiber.memoizedState !== alternate.memoizedState) {\n causes.push({ kind: \"unknown\" });\n }\n return causes;\n }\n\n let currentNode = fiber.memoizedState as HookNode | null;\n let previousNode = alternate.memoizedState as HookNode | null;\n let hasContextHook = false;\n let anyStateHookChanged = false;\n\n for (let i = 0; i < hookTypes.length; i++) {\n const type = hookTypes[i];\n\n // useContext has no linked list node — skip pointer advance\n if (HOOKS_WITHOUT_NODE.has(type)) {\n if (type === \"useContext\") hasContextHook = true;\n continue;\n }\n\n if (STATE_HOOKS.has(type) && currentNode && previousNode) {\n if (!Object.is(currentNode.memoizedState, previousNode.memoizedState)) {\n anyStateHookChanged = true;\n causes.push({\n kind: \"hook\",\n hookIndex: i,\n hookType: type,\n previousValue: previousNode.memoizedState,\n nextValue: currentNode.memoizedState,\n });\n }\n }\n\n currentNode = currentNode?.next ?? null;\n previousNode = previousNode?.next ?? null;\n }\n\n // If no state hook changed but component is self-triggered and has\n // useContext → the context value must have changed.\n if (\n hasContextHook &&\n !anyStateHookChanged &&\n fiber.memoizedProps === alternate.memoizedProps\n ) {\n causes.push({ kind: \"hook\", hookType: \"useContext\" });\n }\n\n // If still nothing and self-triggered, mark unknown\n if (causes.length === 0 && fiber.memoizedProps === alternate.memoizedProps) {\n causes.push({ kind: \"unknown\" });\n }\n\n return causes;\n}\n","import type { HighlightEntry, UpdateCause } from \"./types.js\";\n\n// ────────────────────────────────────────────\n// Value formatting\n// ────────────────────────────────────────────\n\nexport function formatValue(value: unknown, maxLength = 50): string {\n if (value === null) return \"null\";\n if (value === undefined) return \"undefined\";\n if (typeof value === \"boolean\") return String(value);\n if (typeof value === \"number\") return String(value);\n if (typeof value === \"function\")\n return `ƒ ${(value as { name?: string }).name || \"anonymous\"}`;\n\n if (typeof value === \"string\") {\n const truncated =\n value.length > maxLength ? value.slice(0, maxLength) + \"…\" : value;\n return JSON.stringify(truncated);\n }\n\n if (Array.isArray(value)) return `Array(${value.length})`;\n\n if (typeof value === \"object\") {\n const name = (value as object).constructor?.name;\n if (name && name !== \"Object\") return name;\n const keys = Object.keys(value as object);\n if (keys.length <= 3) return `{ ${keys.join(\", \")} }`;\n return `{ ${keys.slice(0, 3).join(\", \")}, … }`;\n }\n\n return String(value);\n}\n\n// ────────────────────────────────────────────\n// Cause formatting\n// ────────────────────────────────────────────\n\n/** Compact summary for overlay labels. */\nexport function formatCausesShort(causes: UpdateCause[]): string {\n if (causes.length === 0) return \"\";\n\n const parts: string[] = [];\n for (const cause of causes) {\n if (cause.kind === \"props\") {\n parts.push(\"props\");\n } else if (cause.kind === \"class-state\") {\n parts.push(\"state\");\n } else if (cause.kind === \"hook\" && cause.hookType) {\n const indexSuffix = cause.hookIndex != null ? `[${cause.hookIndex}]` : \"\";\n parts.push(`${cause.hookType}${indexSuffix}`);\n } else {\n parts.push(\"?\");\n }\n }\n\n return parts.join(\", \");\n}\n\n/** Detailed lines for console output. */\nexport function formatCausesConsole(causes: UpdateCause[]): string[] {\n const lines: string[] = [];\n\n for (const cause of causes) {\n if (cause.kind === \"props\") {\n lines.push(\" ↳ props changed (parent re-rendered)\");\n } else if (cause.kind === \"class-state\") {\n lines.push(\" ↳ class state changed\");\n } else if (cause.kind === \"hook\" && cause.hookType) {\n const indexSuffix =\n cause.hookIndex != null ? `[${cause.hookIndex}]` : \"\";\n const name = `${cause.hookType}${indexSuffix}`;\n\n if (cause.previousValue !== undefined && cause.nextValue !== undefined) {\n lines.push(\n ` ↳ ${name}: ${formatValue(cause.previousValue)} → ${formatValue(cause.nextValue)}`,\n );\n } else {\n lines.push(` ↳ ${name} changed`);\n }\n } else {\n lines.push(\" ↳ unknown cause\");\n }\n }\n\n return lines;\n}\n\n// ────────────────────────────────────────────\n// Console output\n// ────────────────────────────────────────────\n\n/** Log re-renders as a collapsed console group. */\nexport function logRerendersToConsole(\n entries: HighlightEntry[],\n showCauses: boolean,\n) {\n if (entries.length === 0) return;\n\n console.groupCollapsed(\n `%c⚛ ${entries.length} re-render${entries.length > 1 ? \"s\" : \"\"}`,\n \"color: #61dafb; font-weight: bold\",\n );\n\n for (let i = 0; i < entries.length; i++) {\n const entry = entries[i];\n const durationText =\n entry.duration > 0 ? ` (${entry.duration.toFixed(2)}ms)` : \"\";\n console.log(\n `%c${entry.component}%c ${entry.path}${durationText}`,\n \"color: #e8e82e; font-weight: bold\",\n \"color: #888\",\n );\n\n if (showCauses && entry.causes.length > 0) {\n const lines = formatCausesConsole(entry.causes);\n for (const line of lines) {\n console.log(`%c${line}`, \"color: #aaa\");\n }\n }\n }\n\n console.groupEnd();\n}\n","// ────────────────────────────────────────────\n// Per-window overlay infrastructure\n// ────────────────────────────────────────────\n//\n// Performance:\n// - Overlay elements pooled per window, reused via animationend\n// - Single CSS @keyframes per window, no JS timers per element\n\nconst ANIMATION_NAME = \"__rdu-fade\";\n\nconst injectedWindows = new WeakSet<Window>();\n\nfunction ensureStylesheet(win: Window) {\n if (injectedWindows.has(win)) return;\n injectedWindows.add(win);\n\n const style = win.document.createElement(\"style\");\n style.textContent = `\n @keyframes ${ANIMATION_NAME} {\n 0% { opacity: 0; }\n 8% { opacity: var(--rdu-opacity, 1); }\n 40% { opacity: var(--rdu-opacity, 1); }\n 100% { opacity: 0; }\n }\n .${ANIMATION_NAME}-box {\n position: fixed;\n pointer-events: none;\n box-sizing: border-box;\n border-radius: 3px;\n opacity: 0;\n will-change: opacity;\n }\n .${ANIMATION_NAME}-label {\n position: absolute;\n top: -18px;\n left: -1px;\n font: 10px/16px ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace;\n padding: 0 4px;\n color: #fff;\n border-radius: 2px 2px 0 0;\n white-space: nowrap;\n pointer-events: none;\n }\n `;\n win.document.head.appendChild(style);\n}\n\n// ────────────────────────────────────────────\n// Overlay root container\n// ────────────────────────────────────────────\n\nconst overlayRoots = new WeakMap<Window, HTMLDivElement>();\n\nfunction getOverlayRoot(win: Window): HTMLDivElement | null {\n if (win.closed) return null;\n\n const existing = overlayRoots.get(win);\n if (existing?.isConnected) return existing;\n\n ensureStylesheet(win);\n\n const root = win.document.createElement(\"div\");\n root.id = \"__react-debug-updates\";\n Object.assign(root.style, {\n position: \"fixed\",\n top: \"0\",\n left: \"0\",\n width: \"0\",\n height: \"0\",\n overflow: \"visible\",\n pointerEvents: \"none\",\n zIndex: \"2147483647\",\n } satisfies Partial<CSSStyleDeclaration>);\n win.document.body.appendChild(root);\n\n overlayRoots.set(win, root);\n return root;\n}\n\n// ────────────────────────────────────────────\n// Element pool\n// ────────────────────────────────────────────\n\nconst pools = new WeakMap<Window, HTMLDivElement[]>();\n\nexport function acquireOverlay(win: Window): HTMLDivElement | null {\n const root = getOverlayRoot(win);\n if (!root) return null;\n\n let pool = pools.get(win);\n if (!pool) {\n pool = [];\n pools.set(win, pool);\n }\n\n const document = win.document;\n let element = pool.pop();\n\n if (!element) {\n element = document.createElement(\"div\");\n element.className = `${ANIMATION_NAME}-box`;\n\n const label = document.createElement(\"span\");\n label.className = `${ANIMATION_NAME}-label`;\n element.appendChild(label);\n\n element.addEventListener(\"animationend\", () => {\n element!.style.animation = \"none\";\n element!.remove();\n pool!.push(element!);\n });\n }\n\n root.appendChild(element);\n return element;\n}\n\nexport function disposeAllOverlays() {\n const mainRoot = overlayRoots.get(window);\n mainRoot?.remove();\n overlayRoots.delete(window);\n pools.delete(window);\n}\n\nexport const OVERLAY_ANIMATION_NAME = ANIMATION_NAME;\n","import type { OverlayConfig, HighlightEntry } from \"./types.js\";\nimport { formatCausesShort } from \"./format.js\";\nimport { acquireOverlay, OVERLAY_ANIMATION_NAME } from \"./overlay.js\";\n\n// ────────────────────────────────────────────\n// Heat color\n// ────────────────────────────────────────────\n\nfunction heatColor(count: number, alpha: number): string {\n const normalizedCount = Math.min((count - 1) / 7, 1);\n const hue = 200 - normalizedCount * 200;\n const saturation = 85 + normalizedCount * 15;\n const lightness = 55 - normalizedCount * 10;\n return `hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`;\n}\n\n// ────────────────────────────────────────────\n// Highlight scheduler\n// ────────────────────────────────────────────\n//\n// Commit path just pushes lightweight entries (no DOM reads, no formatting).\n// Flush (setInterval): batched rect reads → batched DOM writes.\n\ninterface CoalescedEntry {\n count: number;\n totalDuration: number;\n component: string;\n /** Shallowest fiber depth among coalesced entries (for z-ordering). */\n depth: number;\n domNode: HTMLElement;\n ownerWindow: Window;\n causeSummary: string;\n}\n\nexport function createHighlighter(config: OverlayConfig) {\n let pending: HighlightEntry[] = [];\n let timer: ReturnType<typeof setInterval> | null = null;\n\n function flush() {\n if (pending.length === 0) return;\n\n const batch = pending;\n pending = [];\n\n // Coalesce by DOM node identity\n const map = new Map<HTMLElement, CoalescedEntry>();\n\n for (let i = 0; i < batch.length; i++) {\n const entry = batch[i];\n if (!entry.domNode) continue;\n\n const existing = map.get(entry.domNode);\n if (existing) {\n existing.count++;\n existing.totalDuration += entry.duration;\n existing.depth = Math.min(existing.depth, entry.depth);\n } else {\n const win = entry.domNode.ownerDocument?.defaultView;\n if (!win || win.closed) continue;\n map.set(entry.domNode, {\n count: 1,\n totalDuration: entry.duration,\n component: entry.component,\n depth: entry.depth,\n domNode: entry.domNode,\n ownerWindow: win,\n causeSummary: formatCausesShort(entry.causes),\n });\n }\n }\n\n // Read phase: batch all rect reads\n const toShow: Array<{ coalesced: CoalescedEntry; rect: DOMRect }> = [];\n for (const coalesced of map.values()) {\n const rect = coalesced.domNode.getBoundingClientRect();\n if (rect.width > 0 || rect.height > 0) {\n toShow.push({ coalesced, rect });\n }\n }\n\n // Sort deepest first so parents are appended last (render on top)\n toShow.sort((a, b) => b.coalesced.depth - a.coalesced.depth);\n\n // Write phase: position overlays\n for (let i = 0; i < toShow.length; i++) {\n const { coalesced, rect } = toShow[i];\n const element = acquireOverlay(coalesced.ownerWindow);\n if (!element) continue;\n\n const fillColor = heatColor(coalesced.count, 0.18);\n const borderColor = heatColor(coalesced.count, 0.75);\n const labelBackground = heatColor(coalesced.count, 0.9);\n\n const style = element.style;\n style.top = `${rect.top}px`;\n style.left = `${rect.left}px`;\n style.width = `${rect.width}px`;\n style.height = `${rect.height}px`;\n style.backgroundColor = fillColor;\n style.border = `1.5px solid ${borderColor}`;\n style.setProperty(\"--rdu-opacity\", String(config.opacity));\n style.animation = `${OVERLAY_ANIMATION_NAME} ${config.animationDuration}ms ease-out forwards`;\n\n const label = element.firstElementChild as HTMLElement;\n if (config.showLabels) {\n const countText = coalesced.count > 1 ? ` ×${coalesced.count}` : \"\";\n const durationText =\n coalesced.totalDuration > 0\n ? ` ${coalesced.totalDuration.toFixed(1)}ms`\n : \"\";\n const causeText = coalesced.causeSummary\n ? ` (${coalesced.causeSummary})`\n : \"\";\n label.textContent = `${coalesced.component}${countText}${durationText}${causeText}`;\n label.style.backgroundColor = labelBackground;\n } else {\n label.textContent = \"\";\n label.style.backgroundColor = \"transparent\";\n }\n }\n }\n\n function push(entry: HighlightEntry) {\n pending.push(entry);\n if (!timer) {\n timer = setInterval(flush, config.flushInterval);\n }\n }\n\n function dispose() {\n if (timer) {\n clearInterval(timer);\n timer = null;\n }\n pending = [];\n }\n\n return { push, dispose };\n}\n","import type {\n DevToolsHook,\n FiberRoot,\n HighlightEntry,\n MonitorOptions,\n} from \"./types.js\";\nimport {\n detectRenders,\n findNearestDOMNode,\n getComponentName,\n getFiberPath,\n} from \"./fiber.js\";\nimport { detectCauses } from \"./causes.js\";\nimport { logRerendersToConsole } from \"./format.js\";\nimport { createHighlighter } from \"./highlights.js\";\nimport { disposeAllOverlays } from \"./overlay.js\";\n\n/**\n * Ensure __REACT_DEVTOOLS_GLOBAL_HOOK__ exists on window.\n *\n * React reads this hook during initialization — if it's missing, React will\n * never connect. When this library is imported before React (the recommended\n * setup), we need to create a minimal hook so React can find and register with it.\n *\n * If a hook already exists (e.g. from the React DevTools extension), we leave\n * it untouched and hook into it.\n */\nfunction ensureDevToolsHook(win: Window): DevToolsHook {\n const global = win as unknown as {\n __REACT_DEVTOOLS_GLOBAL_HOOK__?: DevToolsHook;\n };\n\n if (!global.__REACT_DEVTOOLS_GLOBAL_HOOK__) {\n global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {\n supportsFiber: true,\n inject() {\n return 1;\n },\n onCommitFiberRoot() {},\n onCommitFiberUnmount() {},\n onPostCommitFiberRoot() {},\n checkDCE() {},\n } as DevToolsHook;\n }\n\n return global.__REACT_DEVTOOLS_GLOBAL_HOOK__;\n}\n\n/**\n * Start monitoring React re-renders.\n *\n * Hooks into `__REACT_DEVTOOLS_GLOBAL_HOOK__.onCommitFiberRoot` to intercept\n * every React commit. Shows visual highlight overlays on re-rendered DOM nodes\n * and optionally logs re-renders to the console.\n *\n * Call this **before** React renders anything — ideally at the very top of\n * your entry point.\n *\n * Returns a `stop` function to unhook and clean up, or `null` if called\n * in a non-browser environment (e.g. SSR).\n */\nexport function startReactUpdatesMonitor({\n logToConsole = false,\n highlight = true,\n mode = \"self-triggered\",\n reasonOfUpdate = false,\n highlightFlushInterval = 250,\n highlightAnimationDuration = 1200,\n highlightShowLabels = true,\n highlightOpacity = 0.3,\n}: MonitorOptions = {}): (() => void) | null {\n // SSR guard — nothing to do without a DOM\n if (typeof window === \"undefined\") return null;\n\n const hook = ensureDevToolsHook(window);\n\n const highlighter = highlight\n ? createHighlighter({\n flushInterval: highlightFlushInterval,\n animationDuration: highlightAnimationDuration,\n showLabels: highlightShowLabels,\n opacity: highlightOpacity,\n })\n : null;\n\n const previousOnCommit = hook.onCommitFiberRoot.bind(hook);\n\n hook.onCommitFiberRoot = (\n rendererID: number,\n root: FiberRoot,\n priorityLevel?: unknown,\n ) => {\n previousOnCommit(rendererID, root, priorityLevel);\n\n // 1. Detect which component fibers actually re-rendered (pure fiber analysis)\n const detectedRenders = detectRenders(root.current, mode);\n if (detectedRenders.length === 0) return;\n\n // 2. Build full entries: resolve names, DOM nodes, causes\n const highlightEntries: HighlightEntry[] = [];\n\n for (let i = 0; i < detectedRenders.length; i++) {\n const { fiber, depth } = detectedRenders[i];\n const name = getComponentName(fiber);\n if (!name) continue;\n\n const entry: HighlightEntry = {\n component: name,\n path: getFiberPath(fiber),\n duration: fiber.actualDuration ?? 0,\n depth,\n domNode: findNearestDOMNode(fiber),\n causes: reasonOfUpdate ? detectCauses(fiber) : [],\n };\n\n highlightEntries.push(entry);\n highlighter?.push(entry);\n }\n\n // 3. Console output\n if (logToConsole) {\n logRerendersToConsole(highlightEntries, reasonOfUpdate);\n }\n };\n\n if (logToConsole) {\n console.log(\n \"%c⚛ react-debug-updates attached\",\n \"color: #61dafb; font-weight: bold\",\n );\n }\n\n return () => {\n hook.onCommitFiberRoot = previousOnCommit;\n highlighter?.dispose();\n disposeAllOverlays();\n };\n}\n"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -42,7 +42,7 @@ export interface DetectedRender {
|
|
|
42
42
|
fiber: Fiber;
|
|
43
43
|
depth: number;
|
|
44
44
|
}
|
|
45
|
-
export interface
|
|
45
|
+
export interface HighlightEntry {
|
|
46
46
|
component: string;
|
|
47
47
|
path: string;
|
|
48
48
|
duration: number;
|
|
@@ -50,26 +50,13 @@ export interface PendingEntry {
|
|
|
50
50
|
domNode: HTMLElement | null;
|
|
51
51
|
causes: UpdateCause[];
|
|
52
52
|
}
|
|
53
|
-
export interface
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
/** Show text labels (component name, count, duration, cause) above overlays. Default: true */
|
|
59
|
-
showLabels?: boolean;
|
|
60
|
-
/** Peak opacity of overlay highlights (0–1). Default: 0.3 */
|
|
61
|
-
opacity?: number;
|
|
62
|
-
}
|
|
63
|
-
export interface RenderEntry {
|
|
64
|
-
component: string;
|
|
65
|
-
path: string;
|
|
66
|
-
duration: number;
|
|
67
|
-
timestamp: number;
|
|
68
|
-
causes: UpdateCause[];
|
|
53
|
+
export interface OverlayConfig {
|
|
54
|
+
flushInterval: number;
|
|
55
|
+
animationDuration: number;
|
|
56
|
+
showLabels: boolean;
|
|
57
|
+
opacity: number;
|
|
69
58
|
}
|
|
70
|
-
export interface
|
|
71
|
-
/** Suppress console output. Default: false */
|
|
72
|
-
silent?: boolean;
|
|
59
|
+
export interface MonitorOptions {
|
|
73
60
|
/**
|
|
74
61
|
* Which re-renders to track.
|
|
75
62
|
* - `"self-triggered"` — only components whose own state/hooks changed
|
|
@@ -78,12 +65,6 @@ export interface LoggerOptions {
|
|
|
78
65
|
* Default: `"self-triggered"`
|
|
79
66
|
*/
|
|
80
67
|
mode?: "self-triggered" | "all";
|
|
81
|
-
/** Ring buffer size for stored entries. Default: 500 */
|
|
82
|
-
bufferSize?: number;
|
|
83
|
-
/** Filter — return `false` to skip an entry. */
|
|
84
|
-
filter?: (entry: RenderEntry) => boolean;
|
|
85
|
-
/** Enable visual highlight overlays. `true` for defaults, or pass options. */
|
|
86
|
-
highlight?: boolean | HighlightOptions;
|
|
87
68
|
/**
|
|
88
69
|
* Detect and display *why* each component re-rendered.
|
|
89
70
|
*
|
|
@@ -91,11 +72,17 @@ export interface LoggerOptions {
|
|
|
91
72
|
* with previous→next values for state hooks.
|
|
92
73
|
* Requires a React dev build (`_debugHookTypes`). Default: `false`
|
|
93
74
|
*/
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
|
|
75
|
+
reasonOfUpdate?: boolean;
|
|
76
|
+
/** Log re-renders to the console. Default: false */
|
|
77
|
+
logToConsole?: boolean;
|
|
78
|
+
/** Enable visual highlight overlays. Default: true */
|
|
79
|
+
highlight?: boolean;
|
|
80
|
+
/** Show text labels (component name, count, duration, cause) above overlays. Default: true */
|
|
81
|
+
highlightShowLabels?: boolean;
|
|
82
|
+
/** Peak opacity of highlight overlays (0–1). Default: 0.3 */
|
|
83
|
+
highlightOpacity?: number;
|
|
84
|
+
/** Time between highlight flush cycles (ms). Default: 250 */
|
|
85
|
+
highlightFlushInterval?: number;
|
|
86
|
+
/** Highlight fade-out animation duration (ms). Default: 1200 */
|
|
87
|
+
highlightAnimationDuration?: number;
|
|
101
88
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-debug-updates",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "See why React components re-render — visual overlays, console logging, and hook-level cause detection with zero code changes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/react-debug-updates.cjs",
|
package/dist/batcher.d.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { HighlightOptions, PendingEntry } from "./types.js";
|
|
2
|
-
export declare const HIGHLIGHT_DEFAULTS: Required<HighlightOptions>;
|
|
3
|
-
export declare function createBatcher(options: Required<HighlightOptions>): {
|
|
4
|
-
push: (entry: PendingEntry) => void;
|
|
5
|
-
dispose: () => void;
|
|
6
|
-
};
|
package/dist/logger.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { LoggerOptions, RenderLogger } from "./types.js";
|
|
2
|
-
/**
|
|
3
|
-
* Attach a render logger to React's DevTools hook.
|
|
4
|
-
*
|
|
5
|
-
* Hooks into `__REACT_DEVTOOLS_GLOBAL_HOOK__.onCommitFiberRoot` to intercept
|
|
6
|
-
* every React commit. Records re-render entries, optionally logs them to the
|
|
7
|
-
* console, and optionally shows visual highlight overlays on re-rendered DOM nodes.
|
|
8
|
-
*
|
|
9
|
-
* Returns a `RenderLogger` handle, or `null` if the DevTools hook is not found.
|
|
10
|
-
*/
|
|
11
|
-
export declare function attachRenderLogger(options?: LoggerOptions): RenderLogger | null;
|