react-render-detective 0.4.0 → 0.6.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/CHANGELOG.md +299 -0
- package/README.md +2 -2
- package/dist/babel.cjs +2 -2
- package/dist/babel.d.cts +11 -0
- package/dist/babel.d.ts +11 -0
- package/dist/babel.js +1 -1
- package/dist/{chunk-QOGTEVBP.js → chunk-3TZNM7JO.js} +103 -4
- package/dist/chunk-3TZNM7JO.js.map +1 -0
- package/dist/{chunk-HOTY7J3X.js → chunk-D7M4UC3Y.js} +26 -2
- package/dist/chunk-D7M4UC3Y.js.map +1 -0
- package/dist/{chunk-PHYYA67T.cjs → chunk-EXDRRUUQ.cjs} +69 -2
- package/dist/chunk-EXDRRUUQ.cjs.map +1 -0
- package/dist/{chunk-DZ3BZ654.cjs → chunk-LCGXVRJR.cjs} +103 -4
- package/dist/chunk-LCGXVRJR.cjs.map +1 -0
- package/dist/{chunk-MCIQMYNR.js → chunk-QVHAC7AD.js} +69 -2
- package/dist/chunk-QVHAC7AD.js.map +1 -0
- package/dist/{chunk-JD4IJ4MN.cjs → chunk-RGT44QQI.cjs} +26 -2
- package/dist/chunk-RGT44QQI.cjs.map +1 -0
- package/dist/core.cjs +18 -18
- package/dist/core.d.cts +27 -3
- package/dist/core.d.ts +27 -3
- package/dist/core.js +1 -1
- package/dist/index.cjs +197 -295
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -115
- package/dist/index.d.ts +35 -115
- package/dist/index.js +166 -258
- package/dist/index.js.map +1 -1
- package/dist/interactions.cjs +252 -0
- package/dist/interactions.cjs.map +1 -0
- package/dist/interactions.d.cts +110 -0
- package/dist/interactions.d.ts +110 -0
- package/dist/interactions.js +241 -0
- package/dist/interactions.js.map +1 -0
- package/dist/overlay.cjs +5 -5
- package/dist/overlay.js +2 -2
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/{types-gl13xwmN.d.cts → types-CRET8EhB.d.cts} +20 -2
- package/dist/{types-gl13xwmN.d.ts → types-CRET8EhB.d.ts} +20 -2
- package/dist/vite.cjs +2 -2
- package/dist/vite.js +1 -1
- package/package.json +14 -4
- package/dist/chunk-DZ3BZ654.cjs.map +0 -1
- package/dist/chunk-HOTY7J3X.js.map +0 -1
- package/dist/chunk-JD4IJ4MN.cjs.map +0 -1
- package/dist/chunk-MCIQMYNR.js.map +0 -1
- package/dist/chunk-PHYYA67T.cjs.map +0 -1
- package/dist/chunk-QOGTEVBP.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
4
|
-
var chunkDZ3BZ654_cjs = require('./chunk-DZ3BZ654.cjs');
|
|
3
|
+
var chunkRGT44QQI_cjs = require('./chunk-RGT44QQI.cjs');
|
|
5
4
|
var chunkH5RG2EPP_cjs = require('./chunk-H5RG2EPP.cjs');
|
|
5
|
+
var chunkLCGXVRJR_cjs = require('./chunk-LCGXVRJR.cjs');
|
|
6
6
|
var React = require('react');
|
|
7
7
|
var jsxRuntime = require('react/jsx-runtime');
|
|
8
8
|
|
|
@@ -26,6 +26,40 @@ function _interopNamespace(e) {
|
|
|
26
26
|
|
|
27
27
|
var React__namespace = /*#__PURE__*/_interopNamespace(React);
|
|
28
28
|
|
|
29
|
+
// src/console/style.ts
|
|
30
|
+
var PALETTE = {
|
|
31
|
+
plain: "",
|
|
32
|
+
dim: "color:#8a94a6",
|
|
33
|
+
strong: "font-weight:600",
|
|
34
|
+
good: "color:#1f9d5b",
|
|
35
|
+
warn: "color:#c2820a",
|
|
36
|
+
bad: "color:#d1493f;font-weight:600"
|
|
37
|
+
};
|
|
38
|
+
var supportsStyling = () => {
|
|
39
|
+
try {
|
|
40
|
+
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
function styled(segments) {
|
|
46
|
+
if (!supportsStyling()) {
|
|
47
|
+
return [segments.map((s) => typeof s === "string" ? s : s[0]).join("")];
|
|
48
|
+
}
|
|
49
|
+
let format = "";
|
|
50
|
+
const styles = [];
|
|
51
|
+
for (const segment of segments) {
|
|
52
|
+
if (typeof segment === "string") {
|
|
53
|
+
format += segment.replace(/%/g, "%%");
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const [text, tone] = segment;
|
|
57
|
+
format += `%c${text.replace(/%/g, "%%")}%c`;
|
|
58
|
+
styles.push(PALETTE[tone], "");
|
|
59
|
+
}
|
|
60
|
+
return [format, ...styles];
|
|
61
|
+
}
|
|
62
|
+
|
|
29
63
|
// src/console/reporter.ts
|
|
30
64
|
var ICON = {
|
|
31
65
|
normal: "\xB7",
|
|
@@ -34,30 +68,102 @@ var ICON = {
|
|
|
34
68
|
"very-slow": "\u25B2\u25B2",
|
|
35
69
|
critical: "\u25A0"
|
|
36
70
|
};
|
|
71
|
+
var BATCH_WINDOW_MS = 400;
|
|
37
72
|
function attachConsoleReporter(detective) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
73
|
+
let pending = [];
|
|
74
|
+
let timer;
|
|
75
|
+
const flush = () => {
|
|
76
|
+
timer = void 0;
|
|
77
|
+
const batch = pending;
|
|
78
|
+
pending = [];
|
|
79
|
+
if (batch.length === 0) return;
|
|
41
80
|
try {
|
|
42
|
-
|
|
43
|
-
else printConcise(event, detective.config.slowRenderThreshold);
|
|
81
|
+
printBatch(batch, detective.config.slowRenderThreshold);
|
|
44
82
|
} catch {
|
|
45
83
|
}
|
|
84
|
+
};
|
|
85
|
+
const unsubscribe = detective.subscribe((event) => {
|
|
86
|
+
const mode = detective.config.mode;
|
|
87
|
+
if (mode === "silent") return;
|
|
88
|
+
if (mode === "verbose") {
|
|
89
|
+
try {
|
|
90
|
+
printVerbose(event);
|
|
91
|
+
} catch {
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
pending.push(event);
|
|
96
|
+
if (timer === void 0) {
|
|
97
|
+
timer = setTimeout(flush, BATCH_WINDOW_MS);
|
|
98
|
+
timer.unref?.();
|
|
99
|
+
}
|
|
46
100
|
});
|
|
101
|
+
return () => {
|
|
102
|
+
unsubscribe();
|
|
103
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
104
|
+
};
|
|
47
105
|
}
|
|
48
|
-
function
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
106
|
+
function printBatch(batch, slowThreshold) {
|
|
107
|
+
const byComponent = /* @__PURE__ */ new Map();
|
|
108
|
+
let totalMs = 0;
|
|
109
|
+
let avoidable = 0;
|
|
110
|
+
for (const event of batch) {
|
|
111
|
+
const key = event.component.name;
|
|
112
|
+
const entry = byComponent.get(key) ?? {
|
|
113
|
+
name: key,
|
|
114
|
+
source: event.component.source,
|
|
115
|
+
count: 0,
|
|
116
|
+
totalMs: 0,
|
|
117
|
+
reasons: /* @__PURE__ */ new Map(),
|
|
118
|
+
notableReasons: /* @__PURE__ */ new Map(),
|
|
119
|
+
avoidable: 0,
|
|
120
|
+
remount: false,
|
|
121
|
+
slow: false
|
|
122
|
+
};
|
|
123
|
+
entry.count++;
|
|
124
|
+
entry.totalMs += event.timings.selfDuration;
|
|
125
|
+
entry.reasons.set(event.diagnosis.reason, (entry.reasons.get(event.diagnosis.reason) ?? 0) + 1);
|
|
126
|
+
if (event.diagnosis.potentiallyAvoidable || event.timings.selfDuration >= slowThreshold) {
|
|
127
|
+
entry.notableReasons.set(event.diagnosis.reason, (entry.notableReasons.get(event.diagnosis.reason) ?? 0) + 1);
|
|
128
|
+
}
|
|
129
|
+
if (event.diagnosis.potentiallyAvoidable) entry.avoidable++;
|
|
130
|
+
if (event.diagnosis.summary.includes("rebuilt")) entry.remount = true;
|
|
131
|
+
if (event.timings.selfDuration >= slowThreshold) entry.slow = true;
|
|
132
|
+
if (!entry.worst || event.timings.selfDuration > entry.worst.timings.selfDuration) entry.worst = event;
|
|
133
|
+
byComponent.set(key, entry);
|
|
134
|
+
totalMs += event.timings.selfDuration;
|
|
135
|
+
if (event.diagnosis.potentiallyAvoidable) avoidable++;
|
|
136
|
+
}
|
|
137
|
+
const notable = [...byComponent.values()].filter((a) => a.avoidable > 0 || a.slow || a.remount);
|
|
138
|
+
if (notable.length === 0) return;
|
|
139
|
+
const ranked = [...notable].sort((a, b) => b.totalMs - a.totalMs);
|
|
140
|
+
const anySlow = ranked.some((a) => a.slow);
|
|
141
|
+
const segments = [
|
|
142
|
+
["[RRD] ", "dim"],
|
|
143
|
+
[`${batch.length} render${batch.length === 1 ? "" : "s"}`, "strong"],
|
|
144
|
+
[` \xB7 ${totalMs.toFixed(1)}ms`, "dim"]
|
|
145
|
+
];
|
|
146
|
+
if (avoidable > 0) segments.push([` \xB7 ${avoidable} potentially avoidable`, "warn"]);
|
|
147
|
+
for (const a of ranked.slice(0, 8)) {
|
|
148
|
+
const reasonSource = a.notableReasons.size > 0 ? a.notableReasons : a.reasons;
|
|
149
|
+
const reason = [...reasonSource.entries()].sort((x, y) => y[1] - x[1])[0]?.[0] ?? "unknown";
|
|
150
|
+
const tone = a.remount ? "bad" : a.slow ? "warn" : a.avoidable > 0 ? "warn" : "good";
|
|
151
|
+
const label2 = ` ${ICON[a.slow ? "slow" : "normal"]} ${a.name}${a.count > 1 ? ` \xD7${a.count}` : ""}`;
|
|
152
|
+
segments.push("\n", [label2.padEnd(30), tone], [`${a.totalMs.toFixed(1)}ms`, "plain"], [` ${reason}`, "dim"]);
|
|
153
|
+
if (a.remount) segments.push([" rebuilt", "bad"]);
|
|
154
|
+
if (a.avoidable > 0) segments.push([` ${a.avoidable} avoidable`, "warn"]);
|
|
155
|
+
if (a.source) segments.push(["\n " + a.source, "dim"]);
|
|
156
|
+
}
|
|
157
|
+
const worst = ranked[0]?.worst;
|
|
158
|
+
segments.push(
|
|
159
|
+
"\n",
|
|
160
|
+
[
|
|
161
|
+
worst?.diagnosis.suggestion ? ` \u2192 ${worst.diagnosis.suggestion}` : " \u2192 rrd.printOpportunities() to rank everything by recoverable time",
|
|
162
|
+
"dim"
|
|
163
|
+
]
|
|
164
|
+
);
|
|
165
|
+
const log = anySlow ? console.warn : console.log;
|
|
166
|
+
log(...styled(segments));
|
|
61
167
|
}
|
|
62
168
|
function printVerbose(event) {
|
|
63
169
|
const { diagnosis, timings } = event;
|
|
@@ -80,12 +186,10 @@ function printVerbose(event) {
|
|
|
80
186
|
for (const c of event.changedProps) console.log(propLine(c));
|
|
81
187
|
console.groupEnd();
|
|
82
188
|
}
|
|
83
|
-
if (event.unchangedProps.length > 0) {
|
|
84
|
-
console.log(`Props same: ${event.unchangedProps.join(", ")}`);
|
|
85
|
-
}
|
|
189
|
+
if (event.unchangedProps.length > 0) console.log(`Props same: ${event.unchangedProps.join(", ")}`);
|
|
86
190
|
if (event.trackedState.length > 0) {
|
|
87
191
|
console.log(
|
|
88
|
-
`State: ${event.trackedState.map((s) => `${s.name}: ${
|
|
192
|
+
`State: ${event.trackedState.map((s) => `${s.name}: ${chunkLCGXVRJR_cjs.formatInspected(s.previous)} \u2192 ${chunkLCGXVRJR_cjs.formatInspected(s.current)}`).join(", ")}`
|
|
89
193
|
);
|
|
90
194
|
}
|
|
91
195
|
if (event.contextChanges.length > 0) {
|
|
@@ -102,11 +206,11 @@ function printVerbose(event) {
|
|
|
102
206
|
}
|
|
103
207
|
function propLine(c) {
|
|
104
208
|
const head = `${c.key} (${c.valueType}) \u2014 ${describeKind(c)}`;
|
|
105
|
-
if (c.kind === "added") return `${head}: ${
|
|
106
|
-
if (c.kind === "removed") return `${head}: was ${
|
|
209
|
+
if (c.kind === "added") return `${head}: ${chunkLCGXVRJR_cjs.formatInspected(c.current)}`;
|
|
210
|
+
if (c.kind === "removed") return `${head}: was ${chunkLCGXVRJR_cjs.formatInspected(c.previous)}`;
|
|
107
211
|
return `${head}
|
|
108
|
-
previous: ${
|
|
109
|
-
current: ${
|
|
212
|
+
previous: ${chunkLCGXVRJR_cjs.formatInspected(c.previous)}
|
|
213
|
+
current: ${chunkLCGXVRJR_cjs.formatInspected(c.current)}`;
|
|
110
214
|
}
|
|
111
215
|
function describeKind(c) {
|
|
112
216
|
switch (c.kind) {
|
|
@@ -133,6 +237,8 @@ function label(event) {
|
|
|
133
237
|
return "context update";
|
|
134
238
|
case "state":
|
|
135
239
|
return "state changed";
|
|
240
|
+
case "store":
|
|
241
|
+
return "store update";
|
|
136
242
|
case "state-or-external":
|
|
137
243
|
return "state or external store";
|
|
138
244
|
case "unknown":
|
|
@@ -140,194 +246,6 @@ function label(event) {
|
|
|
140
246
|
}
|
|
141
247
|
}
|
|
142
248
|
|
|
143
|
-
// src/core/interactions.ts
|
|
144
|
-
var COMMIT_SLACK_MS = 100;
|
|
145
|
-
var FALLBACK_CLOSE_MS = 50;
|
|
146
|
-
var InteractionTracker = class {
|
|
147
|
-
constructor(capacity = 50) {
|
|
148
|
-
this.capacity = capacity;
|
|
149
|
-
this.records = [];
|
|
150
|
-
this.nextId = 0;
|
|
151
|
-
}
|
|
152
|
-
/** Returns false when the browser cannot report event timing. */
|
|
153
|
-
start() {
|
|
154
|
-
if (this.observer) return true;
|
|
155
|
-
const PO = globalThis.PerformanceObserver;
|
|
156
|
-
const supported = PO?.supportedEntryTypes?.includes("event");
|
|
157
|
-
if (!PO || !supported) return false;
|
|
158
|
-
try {
|
|
159
|
-
const observer = new PO((list) => {
|
|
160
|
-
for (const entry of list.getEntries()) {
|
|
161
|
-
const timing = entry;
|
|
162
|
-
this.record({
|
|
163
|
-
name: timing.name,
|
|
164
|
-
startTime: timing.startTime,
|
|
165
|
-
duration: timing.duration,
|
|
166
|
-
target: describeTarget(timing.target)
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
observer.observe({ type: "event", buffered: true, durationThreshold: 16 });
|
|
171
|
-
this.observer = observer;
|
|
172
|
-
return true;
|
|
173
|
-
} catch {
|
|
174
|
-
return false;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
stop() {
|
|
178
|
-
this.observer?.disconnect();
|
|
179
|
-
this.observer = void 0;
|
|
180
|
-
}
|
|
181
|
-
/** Is the automatic path available in this browser? */
|
|
182
|
-
get automatic() {
|
|
183
|
-
return this.observer !== void 0;
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Time an interaction by hand.
|
|
187
|
-
*
|
|
188
|
-
* The automatic path depends on the Event Timing API, which Safari only
|
|
189
|
-
* gained in 16.4 and which does not fire for synthetic input at all — so
|
|
190
|
-
* anything driven by a test harness records nothing. This measures a specific
|
|
191
|
-
* action instead, up to the paint that follows it, and needs no browser
|
|
192
|
-
* support beyond `performance.now`.
|
|
193
|
-
*/
|
|
194
|
-
measure(label2, action) {
|
|
195
|
-
const startTime = now();
|
|
196
|
-
let handlerMs = 0;
|
|
197
|
-
let finished = false;
|
|
198
|
-
const finish = () => {
|
|
199
|
-
if (finished) return;
|
|
200
|
-
finished = true;
|
|
201
|
-
this.record({ name: label2, startTime, duration: now() - startTime, handlerMs });
|
|
202
|
-
};
|
|
203
|
-
let result;
|
|
204
|
-
try {
|
|
205
|
-
result = action();
|
|
206
|
-
handlerMs = now() - startTime;
|
|
207
|
-
} catch (error) {
|
|
208
|
-
handlerMs = now() - startTime;
|
|
209
|
-
finish();
|
|
210
|
-
throw error;
|
|
211
|
-
}
|
|
212
|
-
const raf = globalThis.requestAnimationFrame;
|
|
213
|
-
if (raf) raf(() => raf(finish));
|
|
214
|
-
setTimeout(finish, FALLBACK_CLOSE_MS);
|
|
215
|
-
return result;
|
|
216
|
-
}
|
|
217
|
-
/** Exposed for tests and for `measure`. */
|
|
218
|
-
record(timing) {
|
|
219
|
-
const record = {
|
|
220
|
-
id: `interaction_${++this.nextId}`,
|
|
221
|
-
type: timing.name,
|
|
222
|
-
target: timing.target,
|
|
223
|
-
startTime: timing.startTime,
|
|
224
|
-
durationMs: timing.duration,
|
|
225
|
-
handlerMs: timing.handlerMs,
|
|
226
|
-
renders: [],
|
|
227
|
-
renderTimeMs: 0,
|
|
228
|
-
avoidableRenderTimeMs: 0
|
|
229
|
-
};
|
|
230
|
-
this.records.push(record);
|
|
231
|
-
if (this.records.length > this.capacity) this.records.shift();
|
|
232
|
-
return record;
|
|
233
|
-
}
|
|
234
|
-
clear() {
|
|
235
|
-
this.records.length = 0;
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Joins render events to interactions by commit time. A render belongs to an
|
|
239
|
-
* interaction when it committed between the event starting and shortly after
|
|
240
|
-
* it finished — React commits just after the event handler returns.
|
|
241
|
-
*/
|
|
242
|
-
attribute(events) {
|
|
243
|
-
for (const record of this.records) {
|
|
244
|
-
const from = record.startTime;
|
|
245
|
-
const to = record.startTime + record.durationMs + COMMIT_SLACK_MS;
|
|
246
|
-
record.renders = events.filter((e) => e.timings.commitTime >= from && e.timings.commitTime <= to);
|
|
247
|
-
record.renderTimeMs = record.renders.reduce((a, e) => a + e.timings.selfDuration, 0);
|
|
248
|
-
record.avoidableRenderTimeMs = record.renders.filter((e) => e.diagnosis.potentiallyAvoidable).reduce((a, e) => a + e.timings.selfDuration, 0);
|
|
249
|
-
}
|
|
250
|
-
return [...this.records].sort((a, b) => b.durationMs - a.durationMs);
|
|
251
|
-
}
|
|
252
|
-
};
|
|
253
|
-
function summarise(record) {
|
|
254
|
-
const byComponent = /* @__PURE__ */ new Map();
|
|
255
|
-
for (const event of record.renders) {
|
|
256
|
-
const entry = byComponent.get(event.component.name) ?? {
|
|
257
|
-
renders: 0,
|
|
258
|
-
totalMs: 0,
|
|
259
|
-
source: event.component.source,
|
|
260
|
-
cause: event.diagnosis.reason
|
|
261
|
-
};
|
|
262
|
-
entry.renders++;
|
|
263
|
-
entry.totalMs += event.timings.selfDuration;
|
|
264
|
-
byComponent.set(event.component.name, entry);
|
|
265
|
-
}
|
|
266
|
-
const contributors = [...byComponent.entries()].map(([component, v]) => ({ component, source: v.source, renders: v.renders, totalMs: v.totalMs, cause: v.cause })).sort((a, b) => b.totalMs - a.totalMs);
|
|
267
|
-
const top = contributors[0];
|
|
268
|
-
const accounted = (record.handlerMs ?? record.durationMs) + record.renderTimeMs;
|
|
269
|
-
const idleWindow = record.handlerMs !== void 0 && record.durationMs > accounted * 3 && record.durationMs - accounted > 100;
|
|
270
|
-
const effectiveMs = idleWindow ? accounted : record.durationMs;
|
|
271
|
-
const share = effectiveMs > 0 ? record.renderTimeMs / effectiveMs : 0;
|
|
272
|
-
let headline;
|
|
273
|
-
let nextStep;
|
|
274
|
-
let confidence = "medium";
|
|
275
|
-
if (idleWindow) {
|
|
276
|
-
headline = `${record.type}: ${fmt(record.handlerMs ?? 0)} in the handler and ${fmt(record.renderTimeMs)} rendering. The measured window was ${fmt(record.durationMs)}, but most of that was the page waiting for a frame \u2014 ignore it.`;
|
|
277
|
-
nextStep = record.renderTimeMs > (record.handlerMs ?? 0) ? `Rendering dominates the real work${top ? `; start with ${top.component}` : ""}.` : "The handler itself costs more than rendering. Profile the handler, not React.";
|
|
278
|
-
return { interaction: record, contributors, headline, nextStep, confidence: "medium" };
|
|
279
|
-
}
|
|
280
|
-
if (record.renders.length === 0) {
|
|
281
|
-
headline = `${record.type} took ${fmt(record.durationMs)}, and no instrumented component rendered inside it.`;
|
|
282
|
-
nextStep = "The cost is somewhere other than React rendering \u2014 an event handler, a layout, or an uninstrumented component. Instrument more of the tree to narrow it down.";
|
|
283
|
-
confidence = "low";
|
|
284
|
-
} else if (share >= 0.4 && record.avoidableRenderTimeMs > 0) {
|
|
285
|
-
headline = `${record.type} took ${fmt(record.durationMs)}; ${fmt(record.renderTimeMs)} of it was rendering, and ${fmt(record.avoidableRenderTimeMs)} of that had no input change to explain it.`;
|
|
286
|
-
nextStep = top ? `Start with ${top.component}${top.source ? ` (${top.source})` : ""} \u2014 ${fmt(top.totalMs)} across ${top.renders} render${top.renders === 1 ? "" : "s"}.` : "Look at the top contributor below.";
|
|
287
|
-
confidence = "high";
|
|
288
|
-
} else if (share >= 0.4) {
|
|
289
|
-
headline = `${record.type} took ${fmt(record.durationMs)}; ${fmt(record.renderTimeMs)} of it was rendering, all of it explained by real input changes.`;
|
|
290
|
-
nextStep = "This is genuine work. Make the renders cheaper rather than fewer \u2014 or do less of it per interaction.";
|
|
291
|
-
confidence = "high";
|
|
292
|
-
} else {
|
|
293
|
-
headline = `${record.type} took ${fmt(record.durationMs)}, but only ${fmt(record.renderTimeMs)} was React rendering.`;
|
|
294
|
-
nextStep = "Most of the cost is outside rendering \u2014 event handlers, layout or paint. A browser profile will show more than this tool can.";
|
|
295
|
-
confidence = "medium";
|
|
296
|
-
}
|
|
297
|
-
return { interaction: record, contributors, headline, nextStep, confidence };
|
|
298
|
-
}
|
|
299
|
-
function formatInteraction(summary) {
|
|
300
|
-
const { interaction: i } = summary;
|
|
301
|
-
const lines = [
|
|
302
|
-
`${i.type}${i.target ? ` on ${i.target}` : ""} ${fmt(i.durationMs)}`,
|
|
303
|
-
"",
|
|
304
|
-
summary.headline
|
|
305
|
-
];
|
|
306
|
-
if (summary.contributors.length > 0) {
|
|
307
|
-
lines.push("", "Rendering inside this interaction");
|
|
308
|
-
for (const c of summary.contributors.slice(0, 8)) {
|
|
309
|
-
lines.push(
|
|
310
|
-
` ${c.component.padEnd(22)} ${String(c.renders).padStart(4)} render(s) ${fmt(c.totalMs).padStart(8)} ${c.cause}`
|
|
311
|
-
);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
lines.push("", `Next step`, ` ${summary.nextStep}`, "", `Confidence: ${summary.confidence}`);
|
|
315
|
-
return lines.join("\n");
|
|
316
|
-
}
|
|
317
|
-
function describeTarget(target) {
|
|
318
|
-
if (!target || typeof target !== "object") return void 0;
|
|
319
|
-
const el = target;
|
|
320
|
-
if (!el.tagName) return void 0;
|
|
321
|
-
const tag = el.tagName.toLowerCase();
|
|
322
|
-
if (el.id) return `${tag}#${el.id}`;
|
|
323
|
-
const className = typeof el.className === "string" ? el.className.trim().split(/\s+/)[0] : void 0;
|
|
324
|
-
if (className) return `${tag}.${className}`;
|
|
325
|
-
const text = el.textContent?.trim().slice(0, 20);
|
|
326
|
-
return text ? `${tag} "${text}"` : tag;
|
|
327
|
-
}
|
|
328
|
-
var fmt = (ms) => `${ms.toFixed(1)}ms`;
|
|
329
|
-
var now = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
330
|
-
|
|
331
249
|
// src/core/opportunities.ts
|
|
332
250
|
var DEFAULT_MIN_SAVING_MS = 1;
|
|
333
251
|
function rankOpportunities({ events, lifecycles, minSavingMs = DEFAULT_MIN_SAVING_MS }) {
|
|
@@ -335,7 +253,7 @@ function rankOpportunities({ events, lifecycles, minSavingMs = DEFAULT_MIN_SAVIN
|
|
|
335
253
|
const out = [];
|
|
336
254
|
for (const name of names) {
|
|
337
255
|
const lifecycle = lifecycles.get(name);
|
|
338
|
-
const explanation =
|
|
256
|
+
const explanation = chunkRGT44QQI_cjs.explainEvents(name, events, lifecycle);
|
|
339
257
|
if (!explanation) continue;
|
|
340
258
|
const mounts = events.filter((e) => e.component.name === name && e.phase === "mount");
|
|
341
259
|
const averageMountCost = mounts.length ? mounts.reduce((a, e) => a + e.timings.selfDuration, 0) / mounts.length : 0;
|
|
@@ -381,7 +299,7 @@ function formatOpportunities(opportunities) {
|
|
|
381
299
|
var AncestryContext = React.createContext(void 0);
|
|
382
300
|
AncestryContext.displayName = "RenderDetectiveAncestry";
|
|
383
301
|
function useInstrumentedNode(name, props, source) {
|
|
384
|
-
const detective =
|
|
302
|
+
const detective = chunkLCGXVRJR_cjs.getDetective();
|
|
385
303
|
const parent = React.useContext(AncestryContext);
|
|
386
304
|
const [node] = React.useState(
|
|
387
305
|
() => detective.enabled ? detective.createNode(name, parent, source) : void 0
|
|
@@ -431,7 +349,7 @@ var IS_WRAPPER = /* @__PURE__ */ Symbol.for("react-render-detective.wrapper");
|
|
|
431
349
|
function withRenderDetective(Component, options = {}) {
|
|
432
350
|
if (Component[IS_WRAPPER]) return Component;
|
|
433
351
|
const name = options.name ?? componentName(Component);
|
|
434
|
-
|
|
352
|
+
chunkLCGXVRJR_cjs.getDetective().noteDefinition(name, options.source, options.declaredInRender === true);
|
|
435
353
|
function RenderDetected(props) {
|
|
436
354
|
const { node, onRender } = useInstrumentedNode(name, props, options.source);
|
|
437
355
|
return renderInstrumented(node, onRender, /* @__PURE__ */ jsxRuntime.jsx(Component, { ...props }));
|
|
@@ -446,8 +364,40 @@ function RenderDetective({ name, children }) {
|
|
|
446
364
|
const { node, onRender } = useInstrumentedNode(name, { children });
|
|
447
365
|
return renderInstrumented(node, onRender, children);
|
|
448
366
|
}
|
|
367
|
+
var useOwnerId = typeof React__namespace.useId === "function" ? React__namespace.useId : () => "";
|
|
368
|
+
function trackSelector(value, options = {}) {
|
|
369
|
+
const detective = chunkLCGXVRJR_cjs.getDetective();
|
|
370
|
+
const node = React.useContext(AncestryContext);
|
|
371
|
+
const ownerId = useOwnerId();
|
|
372
|
+
const previous = React.useRef(void 0);
|
|
373
|
+
if (!detective.enabled) return value;
|
|
374
|
+
if (node && node.stateOwner === void 0) node.stateOwner = ownerId;
|
|
375
|
+
const owns = !node || node.stateOwner === ownerId;
|
|
376
|
+
const prior = previous.current;
|
|
377
|
+
previous.current = { value };
|
|
378
|
+
if (!prior || !node || !owns) return value;
|
|
379
|
+
if (Object.is(prior.value, value)) return value;
|
|
380
|
+
try {
|
|
381
|
+
const equal = chunkLCGXVRJR_cjs.shallowEqual(prior.value, value, detective.config);
|
|
382
|
+
detective.recordSelectorChange(node, {
|
|
383
|
+
name: options.name ?? options.source ?? "selector",
|
|
384
|
+
source: options.source,
|
|
385
|
+
referenceOnly: equal === true,
|
|
386
|
+
previous: chunkLCGXVRJR_cjs.inspect(prior.value, detective.config.inspection),
|
|
387
|
+
current: chunkLCGXVRJR_cjs.inspect(value, detective.config.inspection)
|
|
388
|
+
});
|
|
389
|
+
} catch {
|
|
390
|
+
}
|
|
391
|
+
return value;
|
|
392
|
+
}
|
|
393
|
+
function createTrackedSelectorHook(hook, options = {}) {
|
|
394
|
+
return function useTrackedSelector(...args) {
|
|
395
|
+
const value = hook(...args);
|
|
396
|
+
return trackSelector(value, options);
|
|
397
|
+
};
|
|
398
|
+
}
|
|
449
399
|
function useRenderDiagnostics(name, props) {
|
|
450
|
-
const detective =
|
|
400
|
+
const detective = chunkLCGXVRJR_cjs.getDetective();
|
|
451
401
|
const parent = React.useContext(AncestryContext);
|
|
452
402
|
const [node] = React.useState(() => {
|
|
453
403
|
if (!detective.enabled) return void 0;
|
|
@@ -480,11 +430,11 @@ function useRenderDiagnostics(name, props) {
|
|
|
480
430
|
}
|
|
481
431
|
return last.current;
|
|
482
432
|
}
|
|
483
|
-
var
|
|
433
|
+
var useOwnerId2 = typeof React__namespace.useId === "function" ? React__namespace.useId : () => "";
|
|
484
434
|
function useTrackedState(name, initial) {
|
|
485
|
-
const detective =
|
|
435
|
+
const detective = chunkLCGXVRJR_cjs.getDetective();
|
|
486
436
|
const node = React.useContext(AncestryContext);
|
|
487
|
-
const ownerId =
|
|
437
|
+
const ownerId = useOwnerId2();
|
|
488
438
|
const [state, setState] = React.useState(initial);
|
|
489
439
|
const previous = React.useRef(state);
|
|
490
440
|
if (node && node.stateOwner === void 0) node.stateOwner = ownerId;
|
|
@@ -492,8 +442,8 @@ function useTrackedState(name, initial) {
|
|
|
492
442
|
if (node && owns && !Object.is(previous.current, state)) {
|
|
493
443
|
detective.recordStateChange(node, {
|
|
494
444
|
name,
|
|
495
|
-
previous:
|
|
496
|
-
current:
|
|
445
|
+
previous: chunkLCGXVRJR_cjs.inspect(previous.current, detective.config.inspection),
|
|
446
|
+
current: chunkLCGXVRJR_cjs.inspect(state, detective.config.inspection)
|
|
497
447
|
});
|
|
498
448
|
previous.current = state;
|
|
499
449
|
} else if (!owns) {
|
|
@@ -502,7 +452,7 @@ function useTrackedState(name, initial) {
|
|
|
502
452
|
return [state, setState];
|
|
503
453
|
}
|
|
504
454
|
function useTrackedEffect(name, effect, deps) {
|
|
505
|
-
const detective =
|
|
455
|
+
const detective = chunkLCGXVRJR_cjs.getDetective();
|
|
506
456
|
const previous = React.useRef(void 0);
|
|
507
457
|
const changed = React.useRef([]);
|
|
508
458
|
if (detective.enabled) {
|
|
@@ -518,7 +468,7 @@ function useTrackedEffect(name, effect, deps) {
|
|
|
518
468
|
}, deps);
|
|
519
469
|
}
|
|
520
470
|
function useTrackedContextValue(contextName, value) {
|
|
521
|
-
const detective =
|
|
471
|
+
const detective = chunkLCGXVRJR_cjs.getDetective();
|
|
522
472
|
const previous = React.useRef(void 0);
|
|
523
473
|
const first = React.useRef(true);
|
|
524
474
|
if (detective.enabled) {
|
|
@@ -526,8 +476,8 @@ function useTrackedContextValue(contextName, value) {
|
|
|
526
476
|
first.current = false;
|
|
527
477
|
} else if (!Object.is(previous.current, value)) {
|
|
528
478
|
const prev = previous.current;
|
|
529
|
-
const equal =
|
|
530
|
-
const changedKeys = isRecord(prev) && isRecord(value) ?
|
|
479
|
+
const equal = chunkLCGXVRJR_cjs.shallowEqual(prev, value, detective.config);
|
|
480
|
+
const changedKeys = isRecord(prev) && isRecord(value) ? chunkLCGXVRJR_cjs.diffProps(prev, value, detective.config).changed.map((c) => c.key) : [];
|
|
531
481
|
detective.recordContextChange({
|
|
532
482
|
contextName,
|
|
533
483
|
changedKeys,
|
|
@@ -545,21 +495,12 @@ function isRecord(v) {
|
|
|
545
495
|
|
|
546
496
|
// src/index.ts
|
|
547
497
|
var REPORTER = /* @__PURE__ */ Symbol.for("react-render-detective.reporter");
|
|
548
|
-
var INTERACTIONS = /* @__PURE__ */ Symbol.for("react-render-detective.interactions");
|
|
549
|
-
function tracker() {
|
|
550
|
-
const g = globalThis;
|
|
551
|
-
if (!g[INTERACTIONS]) g[INTERACTIONS] = new InteractionTracker();
|
|
552
|
-
return g[INTERACTIONS];
|
|
553
|
-
}
|
|
554
498
|
function init(options = {}) {
|
|
555
|
-
const detective =
|
|
499
|
+
const detective = chunkLCGXVRJR_cjs.getDetective();
|
|
556
500
|
detective.init(options);
|
|
557
501
|
const g = globalThis;
|
|
558
502
|
g[REPORTER]?.();
|
|
559
503
|
g[REPORTER] = void 0;
|
|
560
|
-
if (detective.enabled) {
|
|
561
|
-
tracker().start();
|
|
562
|
-
}
|
|
563
504
|
if (detective.enabled && detective.config.mode !== "silent") {
|
|
564
505
|
g[REPORTER] = attachConsoleReporter(detective);
|
|
565
506
|
} else if (!detective.enabled) {
|
|
@@ -569,77 +510,49 @@ function init(options = {}) {
|
|
|
569
510
|
}
|
|
570
511
|
}
|
|
571
512
|
function configure(options) {
|
|
572
|
-
|
|
513
|
+
chunkLCGXVRJR_cjs.getDetective().configure(options);
|
|
573
514
|
}
|
|
574
515
|
function getConfig() {
|
|
575
|
-
return
|
|
516
|
+
return chunkLCGXVRJR_cjs.getDetective().config;
|
|
576
517
|
}
|
|
577
518
|
function isEnabled() {
|
|
578
|
-
return
|
|
519
|
+
return chunkLCGXVRJR_cjs.getDetective().enabled;
|
|
579
520
|
}
|
|
580
521
|
function getEvents() {
|
|
581
|
-
return
|
|
522
|
+
return chunkLCGXVRJR_cjs.getDetective().getEvents();
|
|
582
523
|
}
|
|
583
524
|
function getStats() {
|
|
584
|
-
return
|
|
525
|
+
return chunkLCGXVRJR_cjs.getDetective().getStats();
|
|
585
526
|
}
|
|
586
527
|
function getComponentStats(name) {
|
|
587
|
-
return
|
|
528
|
+
return chunkLCGXVRJR_cjs.getDetective().getComponentStats(name);
|
|
588
529
|
}
|
|
589
530
|
function subscribe(listener) {
|
|
590
|
-
return
|
|
531
|
+
return chunkLCGXVRJR_cjs.getDetective().subscribe(listener);
|
|
591
532
|
}
|
|
592
533
|
function clear() {
|
|
593
|
-
|
|
594
|
-
tracker().clear();
|
|
534
|
+
chunkLCGXVRJR_cjs.getDetective().clear();
|
|
595
535
|
}
|
|
596
536
|
function reset() {
|
|
597
537
|
const g = globalThis;
|
|
598
538
|
g[REPORTER]?.();
|
|
599
539
|
g[REPORTER] = void 0;
|
|
600
|
-
|
|
601
|
-
g[INTERACTIONS] = void 0;
|
|
602
|
-
chunkDZ3BZ654_cjs.getDetective().reset();
|
|
540
|
+
chunkLCGXVRJR_cjs.getDetective().reset();
|
|
603
541
|
}
|
|
604
542
|
function explain(componentName2) {
|
|
605
543
|
const explanation = explainStructured(componentName2);
|
|
606
|
-
return explanation ?
|
|
544
|
+
return explanation ? chunkRGT44QQI_cjs.formatExplanation(explanation) : void 0;
|
|
607
545
|
}
|
|
608
546
|
function explainStructured(componentName2) {
|
|
609
|
-
return
|
|
547
|
+
return chunkRGT44QQI_cjs.explainEvents(componentName2, getEvents(), chunkLCGXVRJR_cjs.getDetective().lifecycleOf(componentName2));
|
|
610
548
|
}
|
|
611
549
|
function getRenderProfile(scenario) {
|
|
612
550
|
const remounts = {};
|
|
613
|
-
for (const stats of
|
|
551
|
+
for (const stats of chunkLCGXVRJR_cjs.getDetective().getComponentStats()) remounts[stats.name] = stats.remountCount;
|
|
614
552
|
return chunkH5RG2EPP_cjs.profileFromEvents(scenario, getEvents(), remounts);
|
|
615
553
|
}
|
|
616
|
-
function getInteractions() {
|
|
617
|
-
return tracker().attribute(getEvents());
|
|
618
|
-
}
|
|
619
|
-
function explainInteractionStructured(id) {
|
|
620
|
-
const records = getInteractions();
|
|
621
|
-
const record = id ? records.find((r) => r.id === id) : records[0];
|
|
622
|
-
return record ? summarise(record) : void 0;
|
|
623
|
-
}
|
|
624
|
-
function explainInteraction(id) {
|
|
625
|
-
const summary = explainInteractionStructured(id);
|
|
626
|
-
return summary ? formatInteraction(summary) : void 0;
|
|
627
|
-
}
|
|
628
|
-
function printInteractions(limit = 5) {
|
|
629
|
-
const records = getInteractions().slice(0, limit);
|
|
630
|
-
if (records.length === 0) {
|
|
631
|
-
console.log(
|
|
632
|
-
tracker().automatic ? "No interactions recorded yet. Event timing is working \u2014 nothing has taken longer than 16ms.\nSynthetic clicks from a test harness never produce these entries; use measureInteraction() there." : "This browser does not report event timing (Safari before 16.4, jsdom).\nUse measureInteraction(label, fn) to time interactions by hand."
|
|
633
|
-
);
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
636
|
-
console.log(records.map((r) => formatInteraction(summarise(r))).join("\n\n"));
|
|
637
|
-
}
|
|
638
|
-
function measureInteraction(label2, action) {
|
|
639
|
-
return tracker().measure(label2, action);
|
|
640
|
-
}
|
|
641
554
|
function getOpportunities(limit = 10) {
|
|
642
|
-
const detective =
|
|
555
|
+
const detective = chunkLCGXVRJR_cjs.getDetective();
|
|
643
556
|
const lifecycles = /* @__PURE__ */ new Map();
|
|
644
557
|
for (const stats of detective.getComponentStats()) {
|
|
645
558
|
lifecycles.set(stats.name, { remounts: stats.remountCount });
|
|
@@ -696,51 +609,40 @@ var ReactRenderDetective = {
|
|
|
696
609
|
explainStructured,
|
|
697
610
|
getOpportunities,
|
|
698
611
|
printOpportunities,
|
|
699
|
-
getInteractions,
|
|
700
|
-
explainInteraction,
|
|
701
|
-
explainInteractionStructured,
|
|
702
|
-
printInteractions,
|
|
703
|
-
measureInteraction,
|
|
704
612
|
getRenderProfile,
|
|
705
613
|
printStats
|
|
706
614
|
};
|
|
707
615
|
|
|
708
616
|
Object.defineProperty(exports, "explainEvents", {
|
|
709
617
|
enumerable: true,
|
|
710
|
-
get: function () { return
|
|
618
|
+
get: function () { return chunkRGT44QQI_cjs.explainEvents; }
|
|
711
619
|
});
|
|
712
620
|
Object.defineProperty(exports, "formatExplanation", {
|
|
713
621
|
enumerable: true,
|
|
714
|
-
get: function () { return
|
|
622
|
+
get: function () { return chunkRGT44QQI_cjs.formatExplanation; }
|
|
715
623
|
});
|
|
716
|
-
exports.InteractionTracker = InteractionTracker;
|
|
717
624
|
exports.ReactRenderDetective = ReactRenderDetective;
|
|
718
625
|
exports.RenderDetective = RenderDetective;
|
|
719
626
|
exports.clear = clear;
|
|
720
627
|
exports.configure = configure;
|
|
628
|
+
exports.createTrackedSelectorHook = createTrackedSelectorHook;
|
|
721
629
|
exports.explain = explain;
|
|
722
|
-
exports.explainInteraction = explainInteraction;
|
|
723
|
-
exports.explainInteractionStructured = explainInteractionStructured;
|
|
724
630
|
exports.explainStructured = explainStructured;
|
|
725
|
-
exports.formatInteraction = formatInteraction;
|
|
726
631
|
exports.formatOpportunities = formatOpportunities;
|
|
727
632
|
exports.getComponentStats = getComponentStats;
|
|
728
633
|
exports.getConfig = getConfig;
|
|
729
634
|
exports.getEvents = getEvents;
|
|
730
|
-
exports.getInteractions = getInteractions;
|
|
731
635
|
exports.getOpportunities = getOpportunities;
|
|
732
636
|
exports.getRenderProfile = getRenderProfile;
|
|
733
637
|
exports.getStats = getStats;
|
|
734
638
|
exports.init = init;
|
|
735
639
|
exports.isEnabled = isEnabled;
|
|
736
|
-
exports.measureInteraction = measureInteraction;
|
|
737
|
-
exports.printInteractions = printInteractions;
|
|
738
640
|
exports.printOpportunities = printOpportunities;
|
|
739
641
|
exports.printStats = printStats;
|
|
740
642
|
exports.rankOpportunities = rankOpportunities;
|
|
741
643
|
exports.reset = reset;
|
|
742
644
|
exports.subscribe = subscribe;
|
|
743
|
-
exports.
|
|
645
|
+
exports.trackSelector = trackSelector;
|
|
744
646
|
exports.useRenderDiagnostics = useRenderDiagnostics;
|
|
745
647
|
exports.useTrackedContextValue = useTrackedContextValue;
|
|
746
648
|
exports.useTrackedEffect = useTrackedEffect;
|