react-render-detective 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +278 -0
- package/dist/chunk-7FTXUOB6.cjs +1030 -0
- package/dist/chunk-7FTXUOB6.cjs.map +1 -0
- package/dist/chunk-EGA344XG.js +113 -0
- package/dist/chunk-EGA344XG.js.map +1 -0
- package/dist/chunk-GRPJ4JAB.js +1012 -0
- package/dist/chunk-GRPJ4JAB.js.map +1 -0
- package/dist/chunk-OCW2Q3UW.cjs +116 -0
- package/dist/chunk-OCW2Q3UW.cjs.map +1 -0
- package/dist/core.cjs +76 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.cts +198 -0
- package/dist/core.d.ts +198 -0
- package/dist/core.js +3 -0
- package/dist/core.js.map +1 -0
- package/dist/index.cjs +417 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +153 -0
- package/dist/index.d.ts +153 -0
- package/dist/index.js +370 -0
- package/dist/index.js.map +1 -0
- package/dist/overlay.cjs +147 -0
- package/dist/overlay.cjs.map +1 -0
- package/dist/overlay.d.cts +15 -0
- package/dist/overlay.d.ts +15 -0
- package/dist/overlay.js +145 -0
- package/dist/overlay.js.map +1 -0
- package/dist/types-CY61yfhK.d.cts +225 -0
- package/dist/types-CY61yfhK.d.ts +225 -0
- package/package.json +88 -0
package/dist/overlay.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { explainEvents, formatExplanation } from './chunk-EGA344XG.js';
|
|
2
|
+
import { getDetective } from './chunk-GRPJ4JAB.js';
|
|
3
|
+
|
|
4
|
+
// src/overlay/overlay.ts
|
|
5
|
+
var MOUNTED = /* @__PURE__ */ Symbol.for("react-render-detective.overlay");
|
|
6
|
+
function mountOverlay() {
|
|
7
|
+
const g = globalThis;
|
|
8
|
+
if (g[MOUNTED]) return g[MOUNTED];
|
|
9
|
+
if (typeof document === "undefined") {
|
|
10
|
+
const noop = { destroy: () => {
|
|
11
|
+
}, show: () => {
|
|
12
|
+
}, hide: () => {
|
|
13
|
+
} };
|
|
14
|
+
return noop;
|
|
15
|
+
}
|
|
16
|
+
const detective = getDetective();
|
|
17
|
+
const root = document.createElement("div");
|
|
18
|
+
root.setAttribute("data-rrd-overlay", "");
|
|
19
|
+
root.attachShadow({ mode: "open" });
|
|
20
|
+
const shadow = root.shadowRoot;
|
|
21
|
+
shadow.innerHTML = TEMPLATE;
|
|
22
|
+
document.body.appendChild(root);
|
|
23
|
+
const $ = (sel) => shadow.querySelector(sel);
|
|
24
|
+
const listEl = $("#list");
|
|
25
|
+
const detailEl = $("#detail");
|
|
26
|
+
const filterEl = $("#filter");
|
|
27
|
+
const summaryEl = $("#summary");
|
|
28
|
+
const panelEl = $("#panel");
|
|
29
|
+
let selected;
|
|
30
|
+
let paused = false;
|
|
31
|
+
let dirty = true;
|
|
32
|
+
let filter = "";
|
|
33
|
+
const unsubscribe = detective.subscribe((event) => {
|
|
34
|
+
if (paused) return;
|
|
35
|
+
dirty = true;
|
|
36
|
+
});
|
|
37
|
+
filterEl.addEventListener("input", () => {
|
|
38
|
+
filter = filterEl.value.trim().toLowerCase();
|
|
39
|
+
dirty = true;
|
|
40
|
+
});
|
|
41
|
+
$("#pause").addEventListener("click", () => {
|
|
42
|
+
paused = !paused;
|
|
43
|
+
$("#pause").textContent = paused ? "Resume" : "Pause";
|
|
44
|
+
});
|
|
45
|
+
$("#clear").addEventListener("click", () => {
|
|
46
|
+
detective.clear();
|
|
47
|
+
selected = void 0;
|
|
48
|
+
detailEl.textContent = "";
|
|
49
|
+
dirty = true;
|
|
50
|
+
});
|
|
51
|
+
$("#close").addEventListener("click", () => handle.hide());
|
|
52
|
+
listEl.addEventListener("click", (e) => {
|
|
53
|
+
const row = e.target.closest("[data-name]");
|
|
54
|
+
if (!row) return;
|
|
55
|
+
selected = row.dataset.name;
|
|
56
|
+
dirty = true;
|
|
57
|
+
});
|
|
58
|
+
const timer = setInterval(() => {
|
|
59
|
+
if (!dirty || panelEl.hidden) return;
|
|
60
|
+
dirty = false;
|
|
61
|
+
render();
|
|
62
|
+
}, 400);
|
|
63
|
+
function render() {
|
|
64
|
+
const stats = detective.getStats();
|
|
65
|
+
summaryEl.textContent = `${stats.totalRenders} renders \xB7 ${stats.totalRenderTime.toFixed(0)}ms \xB7 ${stats.potentiallyAvoidableRenders} potentially avoidable` + (stats.devReplays > 0 ? ` \xB7 ${stats.devReplays} dev replays` : "");
|
|
66
|
+
const rows = stats.mostExpensive.concat(stats.mostRendered).filter((s, i, arr) => arr.findIndex((o) => o.name === s.name) === i).filter((s) => !filter || s.name.toLowerCase().includes(filter)).sort((a, b) => b.totalSelfDuration - a.totalSelfDuration).slice(0, 40);
|
|
67
|
+
listEl.innerHTML = rows.map((s) => {
|
|
68
|
+
const waste = s.potentiallyAvoidableRenders;
|
|
69
|
+
const flag = waste > 0 ? `<span class="warn">${waste} avoidable</span>` : "";
|
|
70
|
+
return `<div class="row${s.name === selected ? " sel" : ""}" data-name="${escapeHtml(s.name)}">
|
|
71
|
+
<span class="name">${escapeHtml(s.name)}</span>
|
|
72
|
+
<span class="num">${s.renderCount}</span>
|
|
73
|
+
<span class="num">${s.totalSelfDuration.toFixed(0)}ms</span>
|
|
74
|
+
${flag}
|
|
75
|
+
</div>`;
|
|
76
|
+
}).join("");
|
|
77
|
+
if (selected) {
|
|
78
|
+
const explanation = explainEvents(selected, detective.getEvents());
|
|
79
|
+
detailEl.textContent = explanation ? formatExplanation(explanation) : `No recorded renders for ${selected}.`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const handle = {
|
|
83
|
+
destroy() {
|
|
84
|
+
unsubscribe();
|
|
85
|
+
clearInterval(timer);
|
|
86
|
+
root.remove();
|
|
87
|
+
globalThis[MOUNTED] = void 0;
|
|
88
|
+
},
|
|
89
|
+
show() {
|
|
90
|
+
panelEl.hidden = false;
|
|
91
|
+
dirty = true;
|
|
92
|
+
},
|
|
93
|
+
hide() {
|
|
94
|
+
panelEl.hidden = true;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
g[MOUNTED] = handle;
|
|
98
|
+
return handle;
|
|
99
|
+
}
|
|
100
|
+
function escapeHtml(s) {
|
|
101
|
+
return s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
102
|
+
}
|
|
103
|
+
var TEMPLATE = `
|
|
104
|
+
<style>
|
|
105
|
+
:host { all: initial; }
|
|
106
|
+
#panel {
|
|
107
|
+
position: fixed; right: 16px; bottom: 16px; width: 460px; max-height: 70vh;
|
|
108
|
+
display: flex; flex-direction: column; z-index: 2147483000;
|
|
109
|
+
font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
110
|
+
background: #14161a; color: #e6e6e6; border: 1px solid #2b2f36; border-radius: 10px;
|
|
111
|
+
box-shadow: 0 12px 40px rgba(0,0,0,.45); overflow: hidden;
|
|
112
|
+
}
|
|
113
|
+
header { display: flex; gap: 8px; align-items: center; padding: 8px 10px; background: #1b1e24; border-bottom: 1px solid #2b2f36; }
|
|
114
|
+
header b { font-weight: 600; letter-spacing: .02em; }
|
|
115
|
+
header .spacer { flex: 1; }
|
|
116
|
+
button, input { font: inherit; color: inherit; background: #23272e; border: 1px solid #333842; border-radius: 6px; padding: 3px 8px; }
|
|
117
|
+
button { cursor: pointer; }
|
|
118
|
+
#summary { padding: 6px 10px; color: #9aa4b2; border-bottom: 1px solid #2b2f36; }
|
|
119
|
+
#list { overflow: auto; max-height: 30vh; }
|
|
120
|
+
.row { display: flex; gap: 8px; padding: 4px 10px; cursor: pointer; align-items: baseline; }
|
|
121
|
+
.row:hover { background: #1d2128; }
|
|
122
|
+
.row.sel { background: #263041; }
|
|
123
|
+
.name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
124
|
+
.num { color: #9aa4b2; min-width: 56px; text-align: right; }
|
|
125
|
+
.warn { color: #f2b263; }
|
|
126
|
+
#detail { margin: 0; padding: 10px; overflow: auto; white-space: pre-wrap; border-top: 1px solid #2b2f36; color: #cfd6df; }
|
|
127
|
+
</style>
|
|
128
|
+
<div id="panel">
|
|
129
|
+
<header>
|
|
130
|
+
<b>Render Detective</b>
|
|
131
|
+
<span class="spacer"></span>
|
|
132
|
+
<input id="filter" placeholder="filter" size="10" />
|
|
133
|
+
<button id="pause">Pause</button>
|
|
134
|
+
<button id="clear">Clear</button>
|
|
135
|
+
<button id="close">\xD7</button>
|
|
136
|
+
</header>
|
|
137
|
+
<div id="summary"></div>
|
|
138
|
+
<div id="list"></div>
|
|
139
|
+
<pre id="detail">Select a component to see why it rendered.</pre>
|
|
140
|
+
</div>
|
|
141
|
+
`;
|
|
142
|
+
|
|
143
|
+
export { mountOverlay };
|
|
144
|
+
//# sourceMappingURL=overlay.js.map
|
|
145
|
+
//# sourceMappingURL=overlay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/overlay/overlay.ts"],"names":[],"mappings":"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","file":"overlay.js","sourcesContent":["import { explainEvents, formatExplanation } from \"../core/explain.js\";\nimport { getDetective } from \"../core/store.js\";\nimport type { RenderEvent } from \"../core/types.js\";\n\nexport interface OverlayHandle {\n destroy: () => void;\n show: () => void;\n hide: () => void;\n}\n\nconst MOUNTED = Symbol.for(\"react-render-detective.overlay\");\ntype Global = typeof globalThis & { [MOUNTED]?: OverlayHandle };\n\n/**\n * Development overlay.\n *\n * Deliberately plain DOM: rendering the inspector *inside* the app's React tree\n * would add renders to the tree it is measuring. It consumes the same event\n * model as console mode — no diagnostic logic is duplicated here (§68).\n */\nexport function mountOverlay(): OverlayHandle {\n const g = globalThis as Global;\n if (g[MOUNTED]) return g[MOUNTED] as OverlayHandle;\n if (typeof document === \"undefined\") {\n const noop: OverlayHandle = { destroy: () => {}, show: () => {}, hide: () => {} };\n return noop;\n }\n\n const detective = getDetective();\n const root = document.createElement(\"div\");\n root.setAttribute(\"data-rrd-overlay\", \"\");\n root.attachShadow({ mode: \"open\" });\n const shadow = root.shadowRoot as ShadowRoot;\n shadow.innerHTML = TEMPLATE;\n document.body.appendChild(root);\n\n const $ = <T extends HTMLElement>(sel: string): T => shadow.querySelector(sel) as T;\n const listEl = $<HTMLDivElement>(\"#list\");\n const detailEl = $<HTMLPreElement>(\"#detail\");\n const filterEl = $<HTMLInputElement>(\"#filter\");\n const summaryEl = $<HTMLDivElement>(\"#summary\");\n const panelEl = $<HTMLDivElement>(\"#panel\");\n\n let selected: string | undefined;\n let paused = false;\n let dirty = true;\n let filter = \"\";\n const recent: RenderEvent[] = [];\n\n const unsubscribe = detective.subscribe((event) => {\n if (paused) return;\n recent.push(event);\n if (recent.length > 200) recent.shift();\n dirty = true;\n });\n\n filterEl.addEventListener(\"input\", () => {\n filter = filterEl.value.trim().toLowerCase();\n dirty = true;\n });\n $(\"#pause\").addEventListener(\"click\", () => {\n paused = !paused;\n ($(\"#pause\") as HTMLButtonElement).textContent = paused ? \"Resume\" : \"Pause\";\n });\n $(\"#clear\").addEventListener(\"click\", () => {\n detective.clear();\n recent.length = 0;\n selected = undefined;\n detailEl.textContent = \"\";\n dirty = true;\n });\n $(\"#close\").addEventListener(\"click\", () => handle.hide());\n listEl.addEventListener(\"click\", (e) => {\n const row = (e.target as HTMLElement).closest(\"[data-name]\") as HTMLElement | null;\n if (!row) return;\n selected = row.dataset.name as string;\n dirty = true;\n });\n\n const timer = setInterval(() => {\n if (!dirty || panelEl.hidden) return;\n dirty = false;\n render();\n }, 400);\n\n function render(): void {\n const stats = detective.getStats();\n summaryEl.textContent =\n `${stats.totalRenders} renders · ${stats.totalRenderTime.toFixed(0)}ms · ` +\n `${stats.potentiallyAvoidableRenders} potentially avoidable` +\n (stats.devReplays > 0 ? ` · ${stats.devReplays} dev replays` : \"\");\n\n const rows = stats.mostExpensive\n .concat(stats.mostRendered)\n .filter((s, i, arr) => arr.findIndex((o) => o.name === s.name) === i)\n .filter((s) => !filter || s.name.toLowerCase().includes(filter))\n .sort((a, b) => b.totalSelfDuration - a.totalSelfDuration)\n .slice(0, 40);\n\n listEl.innerHTML = rows\n .map((s) => {\n const waste = s.potentiallyAvoidableRenders;\n const flag = waste > 0 ? `<span class=\"warn\">${waste} avoidable</span>` : \"\";\n return `<div class=\"row${s.name === selected ? \" sel\" : \"\"}\" data-name=\"${escapeHtml(s.name)}\">\n <span class=\"name\">${escapeHtml(s.name)}</span>\n <span class=\"num\">${s.renderCount}</span>\n <span class=\"num\">${s.totalSelfDuration.toFixed(0)}ms</span>\n ${flag}\n </div>`;\n })\n .join(\"\");\n\n if (selected) {\n const explanation = explainEvents(selected, detective.getEvents());\n detailEl.textContent = explanation ? formatExplanation(explanation) : `No recorded renders for ${selected}.`;\n }\n }\n\n const handle: OverlayHandle = {\n destroy() {\n unsubscribe();\n clearInterval(timer);\n root.remove();\n (globalThis as Global)[MOUNTED] = undefined;\n },\n show() {\n panelEl.hidden = false;\n dirty = true;\n },\n hide() {\n panelEl.hidden = true;\n },\n };\n\n g[MOUNTED] = handle;\n return handle;\n}\n\nfunction escapeHtml(s: string): string {\n return s.replace(/[&<>\"]/g, (c) => ({ \"&\": \"&\", \"<\": \"<\", \">\": \">\", '\"': \""\" })[c] as string);\n}\n\nconst TEMPLATE = `\n<style>\n :host { all: initial; }\n #panel {\n position: fixed; right: 16px; bottom: 16px; width: 460px; max-height: 70vh;\n display: flex; flex-direction: column; z-index: 2147483000;\n font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;\n background: #14161a; color: #e6e6e6; border: 1px solid #2b2f36; border-radius: 10px;\n box-shadow: 0 12px 40px rgba(0,0,0,.45); overflow: hidden;\n }\n header { display: flex; gap: 8px; align-items: center; padding: 8px 10px; background: #1b1e24; border-bottom: 1px solid #2b2f36; }\n header b { font-weight: 600; letter-spacing: .02em; }\n header .spacer { flex: 1; }\n button, input { font: inherit; color: inherit; background: #23272e; border: 1px solid #333842; border-radius: 6px; padding: 3px 8px; }\n button { cursor: pointer; }\n #summary { padding: 6px 10px; color: #9aa4b2; border-bottom: 1px solid #2b2f36; }\n #list { overflow: auto; max-height: 30vh; }\n .row { display: flex; gap: 8px; padding: 4px 10px; cursor: pointer; align-items: baseline; }\n .row:hover { background: #1d2128; }\n .row.sel { background: #263041; }\n .name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .num { color: #9aa4b2; min-width: 56px; text-align: right; }\n .warn { color: #f2b263; }\n #detail { margin: 0; padding: 10px; overflow: auto; white-space: pre-wrap; border-top: 1px solid #2b2f36; color: #cfd6df; }\n</style>\n<div id=\"panel\">\n <header>\n <b>Render Detective</b>\n <span class=\"spacer\"></span>\n <input id=\"filter\" placeholder=\"filter\" size=\"10\" />\n <button id=\"pause\">Pause</button>\n <button id=\"clear\">Clear</button>\n <button id=\"close\">×</button>\n </header>\n <div id=\"summary\"></div>\n <div id=\"list\"></div>\n <pre id=\"detail\">Select a component to see why it rendered.</pre>\n</div>\n`;\n"]}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/** Public event & diagnostic model. Stable surface — everything else is internal. */
|
|
2
|
+
type RenderPhase = "mount" | "update" | "nested-update";
|
|
3
|
+
type Confidence = "high" | "medium" | "low";
|
|
4
|
+
/**
|
|
5
|
+
* Why a render happened. `state-or-external` is deliberately fused: without React
|
|
6
|
+
* internals we can prove the cause originated *inside* the component, but not
|
|
7
|
+
* whether it was local state, a store subscription, or a forced update.
|
|
8
|
+
*/
|
|
9
|
+
type RenderReason = "mount" | "props" | "parent" | "context"
|
|
10
|
+
/** Proven local state update — only reported when `useTrackedState` named it. */
|
|
11
|
+
| "state" | "state-or-external" | "unknown";
|
|
12
|
+
type PropChangeKind =
|
|
13
|
+
/** key present in one props object only */
|
|
14
|
+
"added" | "removed"
|
|
15
|
+
/** `Object.is` false and contents differ (or are not comparable) */
|
|
16
|
+
| "value"
|
|
17
|
+
/** `Object.is` false but a bounded shallow compare found identical contents */
|
|
18
|
+
| "reference";
|
|
19
|
+
type PropValueType = "function" | "object" | "array" | "element" | "string" | "number" | "boolean" | "null" | "undefined" | "symbol" | "bigint";
|
|
20
|
+
/** Bounded, non-retaining representation of a value. Never a live reference. */
|
|
21
|
+
type Inspected = {
|
|
22
|
+
t: "primitive";
|
|
23
|
+
v: string | number | boolean | null;
|
|
24
|
+
} | {
|
|
25
|
+
t: "undefined";
|
|
26
|
+
} | {
|
|
27
|
+
t: "nan";
|
|
28
|
+
} | {
|
|
29
|
+
t: "symbol";
|
|
30
|
+
v: string;
|
|
31
|
+
} | {
|
|
32
|
+
t: "bigint";
|
|
33
|
+
v: string;
|
|
34
|
+
} | {
|
|
35
|
+
t: "function";
|
|
36
|
+
name: string;
|
|
37
|
+
} | {
|
|
38
|
+
t: "element";
|
|
39
|
+
name: string;
|
|
40
|
+
} | {
|
|
41
|
+
t: "array";
|
|
42
|
+
length: number;
|
|
43
|
+
items?: Inspected[];
|
|
44
|
+
truncated?: boolean;
|
|
45
|
+
} | {
|
|
46
|
+
t: "object";
|
|
47
|
+
ctor?: string;
|
|
48
|
+
keys?: string[];
|
|
49
|
+
entries?: Record<string, Inspected>;
|
|
50
|
+
truncated?: boolean;
|
|
51
|
+
} | {
|
|
52
|
+
t: "circular";
|
|
53
|
+
} | {
|
|
54
|
+
t: "truncated";
|
|
55
|
+
hint: string;
|
|
56
|
+
};
|
|
57
|
+
interface PropChange {
|
|
58
|
+
key: string;
|
|
59
|
+
kind: PropChangeKind;
|
|
60
|
+
valueType: PropValueType;
|
|
61
|
+
/**
|
|
62
|
+
* `true` — reference changed, bounded shallow compare says contents are identical.
|
|
63
|
+
* `false` — contents genuinely differ.
|
|
64
|
+
* `undefined` — not shallow-comparable (function, element, mixed types).
|
|
65
|
+
*/
|
|
66
|
+
shallowEqual?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Only set when `compareFunctionSource` is enabled and both values are
|
|
69
|
+
* functions: their source text is identical, which is evidence of an inline
|
|
70
|
+
* closure being recreated — not evidence that they are interchangeable.
|
|
71
|
+
*/
|
|
72
|
+
sourceEqual?: boolean;
|
|
73
|
+
previous?: Inspected;
|
|
74
|
+
current?: Inspected;
|
|
75
|
+
}
|
|
76
|
+
interface ComponentInfo {
|
|
77
|
+
/** Stable per mounted instance. */
|
|
78
|
+
id: string;
|
|
79
|
+
name: string;
|
|
80
|
+
/** Nearest *instrumented* ancestor — not necessarily the direct parent element. */
|
|
81
|
+
parentId?: string;
|
|
82
|
+
depth: number;
|
|
83
|
+
}
|
|
84
|
+
interface RenderTimings {
|
|
85
|
+
/** Profiler `actualDuration` — this component **and its whole subtree**. */
|
|
86
|
+
subtreeDuration: number;
|
|
87
|
+
/** Profiler `baseDuration` — subtree cost without memoization. */
|
|
88
|
+
baseDuration: number;
|
|
89
|
+
/**
|
|
90
|
+
* `subtreeDuration` minus the duration of instrumented descendants that
|
|
91
|
+
* rendered in the same commit. React exposes no true per-component self time,
|
|
92
|
+
* so this is an **upper bound**: it still contains any *uninstrumented*
|
|
93
|
+
* descendants' work.
|
|
94
|
+
*/
|
|
95
|
+
selfDuration: number;
|
|
96
|
+
/** How much descendant time was subtracted to get `selfDuration`. */
|
|
97
|
+
accountedDescendantDuration: number;
|
|
98
|
+
/** Shared by every component in the same React commit. */
|
|
99
|
+
commitTime: number;
|
|
100
|
+
startTime: number;
|
|
101
|
+
}
|
|
102
|
+
interface Diagnosis {
|
|
103
|
+
reason: RenderReason;
|
|
104
|
+
confidence: Confidence;
|
|
105
|
+
/** One line: what happened. */
|
|
106
|
+
summary: string;
|
|
107
|
+
/** Ordered supporting facts, most relevant first. */
|
|
108
|
+
evidence: string[];
|
|
109
|
+
/** Present only when evidence supports it. Never a blanket "add memo". */
|
|
110
|
+
suggestion?: string;
|
|
111
|
+
/** `true` only when observable inputs did not change at all. */
|
|
112
|
+
potentiallyAvoidable: boolean;
|
|
113
|
+
severity: "normal" | "monitor" | "slow" | "very-slow" | "critical";
|
|
114
|
+
}
|
|
115
|
+
interface RenderEvent {
|
|
116
|
+
id: string;
|
|
117
|
+
component: ComponentInfo;
|
|
118
|
+
/** ms, `performance.now()` domain. */
|
|
119
|
+
timestamp: number;
|
|
120
|
+
/** Committed renders only, 1-based. */
|
|
121
|
+
renderNumber: number;
|
|
122
|
+
phase: RenderPhase;
|
|
123
|
+
timings: RenderTimings;
|
|
124
|
+
changedProps: PropChange[];
|
|
125
|
+
unchangedProps: string[];
|
|
126
|
+
parent?: ComponentInfo;
|
|
127
|
+
/** Nearest instrumented ancestor re-rendered in this same commit. */
|
|
128
|
+
parentRendered: boolean;
|
|
129
|
+
/**
|
|
130
|
+
* The render started at this component (state, a store, or context) rather
|
|
131
|
+
* than being handed down new props from above.
|
|
132
|
+
*/
|
|
133
|
+
selfOriginated: boolean;
|
|
134
|
+
/** Tracked context values that changed in this commit. */
|
|
135
|
+
contextChanges: ContextChange[];
|
|
136
|
+
/** State updates named via `useTrackedState`. */
|
|
137
|
+
trackedState: TrackedStateChange[];
|
|
138
|
+
committed: boolean;
|
|
139
|
+
/**
|
|
140
|
+
* Render-function invocations for this commit. `> 1` means a development
|
|
141
|
+
* replay (StrictMode double-invoke or a discarded concurrent attempt).
|
|
142
|
+
*/
|
|
143
|
+
attempts: number;
|
|
144
|
+
devReplay: boolean;
|
|
145
|
+
diagnosis: Diagnosis;
|
|
146
|
+
}
|
|
147
|
+
interface ContextChange {
|
|
148
|
+
contextName: string;
|
|
149
|
+
changedKeys: string[];
|
|
150
|
+
/** Whole value reference changed but its shallow contents did not. */
|
|
151
|
+
referenceOnly: boolean;
|
|
152
|
+
commitTime: number;
|
|
153
|
+
}
|
|
154
|
+
interface ComponentStats {
|
|
155
|
+
id: string;
|
|
156
|
+
name: string;
|
|
157
|
+
renderCount: number;
|
|
158
|
+
mountCount: number;
|
|
159
|
+
uncommittedAttempts: number;
|
|
160
|
+
devReplays: number;
|
|
161
|
+
totalSelfDuration: number;
|
|
162
|
+
averageSelfDuration: number;
|
|
163
|
+
medianSelfDuration: number;
|
|
164
|
+
p95SelfDuration: number;
|
|
165
|
+
p99SelfDuration: number;
|
|
166
|
+
maxSelfDuration: number;
|
|
167
|
+
slowRenders: number;
|
|
168
|
+
potentiallyAvoidableRenders: number;
|
|
169
|
+
reasons: Record<RenderReason, number>;
|
|
170
|
+
}
|
|
171
|
+
interface AppStats {
|
|
172
|
+
components: number;
|
|
173
|
+
totalRenders: number;
|
|
174
|
+
totalRenderTime: number;
|
|
175
|
+
slowRenders: number;
|
|
176
|
+
potentiallyAvoidableRenders: number;
|
|
177
|
+
devReplays: number;
|
|
178
|
+
slowest: ComponentStats[];
|
|
179
|
+
mostRendered: ComponentStats[];
|
|
180
|
+
mostExpensive: ComponentStats[];
|
|
181
|
+
}
|
|
182
|
+
type Mode = "silent" | "console" | "verbose";
|
|
183
|
+
interface Thresholds {
|
|
184
|
+
monitor: number;
|
|
185
|
+
slow: number;
|
|
186
|
+
verySlow: number;
|
|
187
|
+
critical: number;
|
|
188
|
+
}
|
|
189
|
+
interface InspectionLimits {
|
|
190
|
+
depth: number;
|
|
191
|
+
maxObjectKeys: number;
|
|
192
|
+
maxArrayLength: number;
|
|
193
|
+
maxStringLength: number;
|
|
194
|
+
maxSerializedNodes: number;
|
|
195
|
+
}
|
|
196
|
+
interface DetectiveConfig {
|
|
197
|
+
enabled: boolean;
|
|
198
|
+
mode: Mode;
|
|
199
|
+
/** Only these components are instrumented. Empty = all. */
|
|
200
|
+
include: Array<string | RegExp>;
|
|
201
|
+
exclude: Array<string | RegExp>;
|
|
202
|
+
/** 0..1 — fraction of *components* sampled, decided once per instance. */
|
|
203
|
+
samplingRate: number;
|
|
204
|
+
/** Ring-buffer capacity. */
|
|
205
|
+
maxEvents: number;
|
|
206
|
+
/** ms. Renders at or above this are flagged slow. */
|
|
207
|
+
slowRenderThreshold: number;
|
|
208
|
+
thresholds: Thresholds;
|
|
209
|
+
inspection: InspectionLimits;
|
|
210
|
+
/** Compare function source text to spot recreated inline callbacks. Off by default. */
|
|
211
|
+
compareFunctionSource: boolean;
|
|
212
|
+
onEvent?: (event: RenderEvent) => void;
|
|
213
|
+
}
|
|
214
|
+
type DetectiveOptions = Partial<Omit<DetectiveConfig, "thresholds" | "inspection">> & {
|
|
215
|
+
thresholds?: Partial<Thresholds>;
|
|
216
|
+
inspection?: Partial<InspectionLimits>;
|
|
217
|
+
};
|
|
218
|
+
interface TrackedStateChange {
|
|
219
|
+
/** Label passed to `useTrackedState`. */
|
|
220
|
+
name: string;
|
|
221
|
+
previous: Inspected;
|
|
222
|
+
current: Inspected;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export type { AppStats as A, ContextChange as C, DetectiveConfig as D, Inspected as I, Mode as M, PropChange as P, RenderReason as R, TrackedStateChange as T, DetectiveOptions as a, RenderEvent as b, RenderPhase as c, ComponentStats as d, Thresholds as e, Diagnosis as f, InspectionLimits as g, PropValueType as h, ComponentInfo as i, Confidence as j, PropChangeKind as k, RenderTimings as l };
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/** Public event & diagnostic model. Stable surface — everything else is internal. */
|
|
2
|
+
type RenderPhase = "mount" | "update" | "nested-update";
|
|
3
|
+
type Confidence = "high" | "medium" | "low";
|
|
4
|
+
/**
|
|
5
|
+
* Why a render happened. `state-or-external` is deliberately fused: without React
|
|
6
|
+
* internals we can prove the cause originated *inside* the component, but not
|
|
7
|
+
* whether it was local state, a store subscription, or a forced update.
|
|
8
|
+
*/
|
|
9
|
+
type RenderReason = "mount" | "props" | "parent" | "context"
|
|
10
|
+
/** Proven local state update — only reported when `useTrackedState` named it. */
|
|
11
|
+
| "state" | "state-or-external" | "unknown";
|
|
12
|
+
type PropChangeKind =
|
|
13
|
+
/** key present in one props object only */
|
|
14
|
+
"added" | "removed"
|
|
15
|
+
/** `Object.is` false and contents differ (or are not comparable) */
|
|
16
|
+
| "value"
|
|
17
|
+
/** `Object.is` false but a bounded shallow compare found identical contents */
|
|
18
|
+
| "reference";
|
|
19
|
+
type PropValueType = "function" | "object" | "array" | "element" | "string" | "number" | "boolean" | "null" | "undefined" | "symbol" | "bigint";
|
|
20
|
+
/** Bounded, non-retaining representation of a value. Never a live reference. */
|
|
21
|
+
type Inspected = {
|
|
22
|
+
t: "primitive";
|
|
23
|
+
v: string | number | boolean | null;
|
|
24
|
+
} | {
|
|
25
|
+
t: "undefined";
|
|
26
|
+
} | {
|
|
27
|
+
t: "nan";
|
|
28
|
+
} | {
|
|
29
|
+
t: "symbol";
|
|
30
|
+
v: string;
|
|
31
|
+
} | {
|
|
32
|
+
t: "bigint";
|
|
33
|
+
v: string;
|
|
34
|
+
} | {
|
|
35
|
+
t: "function";
|
|
36
|
+
name: string;
|
|
37
|
+
} | {
|
|
38
|
+
t: "element";
|
|
39
|
+
name: string;
|
|
40
|
+
} | {
|
|
41
|
+
t: "array";
|
|
42
|
+
length: number;
|
|
43
|
+
items?: Inspected[];
|
|
44
|
+
truncated?: boolean;
|
|
45
|
+
} | {
|
|
46
|
+
t: "object";
|
|
47
|
+
ctor?: string;
|
|
48
|
+
keys?: string[];
|
|
49
|
+
entries?: Record<string, Inspected>;
|
|
50
|
+
truncated?: boolean;
|
|
51
|
+
} | {
|
|
52
|
+
t: "circular";
|
|
53
|
+
} | {
|
|
54
|
+
t: "truncated";
|
|
55
|
+
hint: string;
|
|
56
|
+
};
|
|
57
|
+
interface PropChange {
|
|
58
|
+
key: string;
|
|
59
|
+
kind: PropChangeKind;
|
|
60
|
+
valueType: PropValueType;
|
|
61
|
+
/**
|
|
62
|
+
* `true` — reference changed, bounded shallow compare says contents are identical.
|
|
63
|
+
* `false` — contents genuinely differ.
|
|
64
|
+
* `undefined` — not shallow-comparable (function, element, mixed types).
|
|
65
|
+
*/
|
|
66
|
+
shallowEqual?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Only set when `compareFunctionSource` is enabled and both values are
|
|
69
|
+
* functions: their source text is identical, which is evidence of an inline
|
|
70
|
+
* closure being recreated — not evidence that they are interchangeable.
|
|
71
|
+
*/
|
|
72
|
+
sourceEqual?: boolean;
|
|
73
|
+
previous?: Inspected;
|
|
74
|
+
current?: Inspected;
|
|
75
|
+
}
|
|
76
|
+
interface ComponentInfo {
|
|
77
|
+
/** Stable per mounted instance. */
|
|
78
|
+
id: string;
|
|
79
|
+
name: string;
|
|
80
|
+
/** Nearest *instrumented* ancestor — not necessarily the direct parent element. */
|
|
81
|
+
parentId?: string;
|
|
82
|
+
depth: number;
|
|
83
|
+
}
|
|
84
|
+
interface RenderTimings {
|
|
85
|
+
/** Profiler `actualDuration` — this component **and its whole subtree**. */
|
|
86
|
+
subtreeDuration: number;
|
|
87
|
+
/** Profiler `baseDuration` — subtree cost without memoization. */
|
|
88
|
+
baseDuration: number;
|
|
89
|
+
/**
|
|
90
|
+
* `subtreeDuration` minus the duration of instrumented descendants that
|
|
91
|
+
* rendered in the same commit. React exposes no true per-component self time,
|
|
92
|
+
* so this is an **upper bound**: it still contains any *uninstrumented*
|
|
93
|
+
* descendants' work.
|
|
94
|
+
*/
|
|
95
|
+
selfDuration: number;
|
|
96
|
+
/** How much descendant time was subtracted to get `selfDuration`. */
|
|
97
|
+
accountedDescendantDuration: number;
|
|
98
|
+
/** Shared by every component in the same React commit. */
|
|
99
|
+
commitTime: number;
|
|
100
|
+
startTime: number;
|
|
101
|
+
}
|
|
102
|
+
interface Diagnosis {
|
|
103
|
+
reason: RenderReason;
|
|
104
|
+
confidence: Confidence;
|
|
105
|
+
/** One line: what happened. */
|
|
106
|
+
summary: string;
|
|
107
|
+
/** Ordered supporting facts, most relevant first. */
|
|
108
|
+
evidence: string[];
|
|
109
|
+
/** Present only when evidence supports it. Never a blanket "add memo". */
|
|
110
|
+
suggestion?: string;
|
|
111
|
+
/** `true` only when observable inputs did not change at all. */
|
|
112
|
+
potentiallyAvoidable: boolean;
|
|
113
|
+
severity: "normal" | "monitor" | "slow" | "very-slow" | "critical";
|
|
114
|
+
}
|
|
115
|
+
interface RenderEvent {
|
|
116
|
+
id: string;
|
|
117
|
+
component: ComponentInfo;
|
|
118
|
+
/** ms, `performance.now()` domain. */
|
|
119
|
+
timestamp: number;
|
|
120
|
+
/** Committed renders only, 1-based. */
|
|
121
|
+
renderNumber: number;
|
|
122
|
+
phase: RenderPhase;
|
|
123
|
+
timings: RenderTimings;
|
|
124
|
+
changedProps: PropChange[];
|
|
125
|
+
unchangedProps: string[];
|
|
126
|
+
parent?: ComponentInfo;
|
|
127
|
+
/** Nearest instrumented ancestor re-rendered in this same commit. */
|
|
128
|
+
parentRendered: boolean;
|
|
129
|
+
/**
|
|
130
|
+
* The render started at this component (state, a store, or context) rather
|
|
131
|
+
* than being handed down new props from above.
|
|
132
|
+
*/
|
|
133
|
+
selfOriginated: boolean;
|
|
134
|
+
/** Tracked context values that changed in this commit. */
|
|
135
|
+
contextChanges: ContextChange[];
|
|
136
|
+
/** State updates named via `useTrackedState`. */
|
|
137
|
+
trackedState: TrackedStateChange[];
|
|
138
|
+
committed: boolean;
|
|
139
|
+
/**
|
|
140
|
+
* Render-function invocations for this commit. `> 1` means a development
|
|
141
|
+
* replay (StrictMode double-invoke or a discarded concurrent attempt).
|
|
142
|
+
*/
|
|
143
|
+
attempts: number;
|
|
144
|
+
devReplay: boolean;
|
|
145
|
+
diagnosis: Diagnosis;
|
|
146
|
+
}
|
|
147
|
+
interface ContextChange {
|
|
148
|
+
contextName: string;
|
|
149
|
+
changedKeys: string[];
|
|
150
|
+
/** Whole value reference changed but its shallow contents did not. */
|
|
151
|
+
referenceOnly: boolean;
|
|
152
|
+
commitTime: number;
|
|
153
|
+
}
|
|
154
|
+
interface ComponentStats {
|
|
155
|
+
id: string;
|
|
156
|
+
name: string;
|
|
157
|
+
renderCount: number;
|
|
158
|
+
mountCount: number;
|
|
159
|
+
uncommittedAttempts: number;
|
|
160
|
+
devReplays: number;
|
|
161
|
+
totalSelfDuration: number;
|
|
162
|
+
averageSelfDuration: number;
|
|
163
|
+
medianSelfDuration: number;
|
|
164
|
+
p95SelfDuration: number;
|
|
165
|
+
p99SelfDuration: number;
|
|
166
|
+
maxSelfDuration: number;
|
|
167
|
+
slowRenders: number;
|
|
168
|
+
potentiallyAvoidableRenders: number;
|
|
169
|
+
reasons: Record<RenderReason, number>;
|
|
170
|
+
}
|
|
171
|
+
interface AppStats {
|
|
172
|
+
components: number;
|
|
173
|
+
totalRenders: number;
|
|
174
|
+
totalRenderTime: number;
|
|
175
|
+
slowRenders: number;
|
|
176
|
+
potentiallyAvoidableRenders: number;
|
|
177
|
+
devReplays: number;
|
|
178
|
+
slowest: ComponentStats[];
|
|
179
|
+
mostRendered: ComponentStats[];
|
|
180
|
+
mostExpensive: ComponentStats[];
|
|
181
|
+
}
|
|
182
|
+
type Mode = "silent" | "console" | "verbose";
|
|
183
|
+
interface Thresholds {
|
|
184
|
+
monitor: number;
|
|
185
|
+
slow: number;
|
|
186
|
+
verySlow: number;
|
|
187
|
+
critical: number;
|
|
188
|
+
}
|
|
189
|
+
interface InspectionLimits {
|
|
190
|
+
depth: number;
|
|
191
|
+
maxObjectKeys: number;
|
|
192
|
+
maxArrayLength: number;
|
|
193
|
+
maxStringLength: number;
|
|
194
|
+
maxSerializedNodes: number;
|
|
195
|
+
}
|
|
196
|
+
interface DetectiveConfig {
|
|
197
|
+
enabled: boolean;
|
|
198
|
+
mode: Mode;
|
|
199
|
+
/** Only these components are instrumented. Empty = all. */
|
|
200
|
+
include: Array<string | RegExp>;
|
|
201
|
+
exclude: Array<string | RegExp>;
|
|
202
|
+
/** 0..1 — fraction of *components* sampled, decided once per instance. */
|
|
203
|
+
samplingRate: number;
|
|
204
|
+
/** Ring-buffer capacity. */
|
|
205
|
+
maxEvents: number;
|
|
206
|
+
/** ms. Renders at or above this are flagged slow. */
|
|
207
|
+
slowRenderThreshold: number;
|
|
208
|
+
thresholds: Thresholds;
|
|
209
|
+
inspection: InspectionLimits;
|
|
210
|
+
/** Compare function source text to spot recreated inline callbacks. Off by default. */
|
|
211
|
+
compareFunctionSource: boolean;
|
|
212
|
+
onEvent?: (event: RenderEvent) => void;
|
|
213
|
+
}
|
|
214
|
+
type DetectiveOptions = Partial<Omit<DetectiveConfig, "thresholds" | "inspection">> & {
|
|
215
|
+
thresholds?: Partial<Thresholds>;
|
|
216
|
+
inspection?: Partial<InspectionLimits>;
|
|
217
|
+
};
|
|
218
|
+
interface TrackedStateChange {
|
|
219
|
+
/** Label passed to `useTrackedState`. */
|
|
220
|
+
name: string;
|
|
221
|
+
previous: Inspected;
|
|
222
|
+
current: Inspected;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export type { AppStats as A, ContextChange as C, DetectiveConfig as D, Inspected as I, Mode as M, PropChange as P, RenderReason as R, TrackedStateChange as T, DetectiveOptions as a, RenderEvent as b, RenderPhase as c, ComponentStats as d, Thresholds as e, Diagnosis as f, InspectionLimits as g, PropValueType as h, ComponentInfo as i, Confidence as j, PropChangeKind as k, RenderTimings as l };
|