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.js
CHANGED
|
@@ -1,11 +1,45 @@
|
|
|
1
|
-
import { explainEvents, formatExplanation } from './chunk-
|
|
2
|
-
export { explainEvents, formatExplanation } from './chunk-
|
|
3
|
-
import { getDetective, inspect, shallowEqual, diffProps, formatInspected } from './chunk-QOGTEVBP.js';
|
|
1
|
+
import { explainEvents, formatExplanation } from './chunk-D7M4UC3Y.js';
|
|
2
|
+
export { explainEvents, formatExplanation } from './chunk-D7M4UC3Y.js';
|
|
4
3
|
import { profileFromEvents } from './chunk-LILK23YH.js';
|
|
4
|
+
import { getDetective, shallowEqual, inspect, diffProps, formatInspected } from './chunk-3TZNM7JO.js';
|
|
5
5
|
import * as React from 'react';
|
|
6
|
-
import { createContext, useContext,
|
|
6
|
+
import { createContext, useContext, useRef, useState, useEffect, useLayoutEffect, useCallback, Profiler } from 'react';
|
|
7
7
|
import { jsx } from 'react/jsx-runtime';
|
|
8
8
|
|
|
9
|
+
// src/console/style.ts
|
|
10
|
+
var PALETTE = {
|
|
11
|
+
plain: "",
|
|
12
|
+
dim: "color:#8a94a6",
|
|
13
|
+
strong: "font-weight:600",
|
|
14
|
+
good: "color:#1f9d5b",
|
|
15
|
+
warn: "color:#c2820a",
|
|
16
|
+
bad: "color:#d1493f;font-weight:600"
|
|
17
|
+
};
|
|
18
|
+
var supportsStyling = () => {
|
|
19
|
+
try {
|
|
20
|
+
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
function styled(segments) {
|
|
26
|
+
if (!supportsStyling()) {
|
|
27
|
+
return [segments.map((s) => typeof s === "string" ? s : s[0]).join("")];
|
|
28
|
+
}
|
|
29
|
+
let format = "";
|
|
30
|
+
const styles = [];
|
|
31
|
+
for (const segment of segments) {
|
|
32
|
+
if (typeof segment === "string") {
|
|
33
|
+
format += segment.replace(/%/g, "%%");
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const [text, tone] = segment;
|
|
37
|
+
format += `%c${text.replace(/%/g, "%%")}%c`;
|
|
38
|
+
styles.push(PALETTE[tone], "");
|
|
39
|
+
}
|
|
40
|
+
return [format, ...styles];
|
|
41
|
+
}
|
|
42
|
+
|
|
9
43
|
// src/console/reporter.ts
|
|
10
44
|
var ICON = {
|
|
11
45
|
normal: "\xB7",
|
|
@@ -14,30 +48,102 @@ var ICON = {
|
|
|
14
48
|
"very-slow": "\u25B2\u25B2",
|
|
15
49
|
critical: "\u25A0"
|
|
16
50
|
};
|
|
51
|
+
var BATCH_WINDOW_MS = 400;
|
|
17
52
|
function attachConsoleReporter(detective) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
53
|
+
let pending = [];
|
|
54
|
+
let timer;
|
|
55
|
+
const flush = () => {
|
|
56
|
+
timer = void 0;
|
|
57
|
+
const batch = pending;
|
|
58
|
+
pending = [];
|
|
59
|
+
if (batch.length === 0) return;
|
|
21
60
|
try {
|
|
22
|
-
|
|
23
|
-
else printConcise(event, detective.config.slowRenderThreshold);
|
|
61
|
+
printBatch(batch, detective.config.slowRenderThreshold);
|
|
24
62
|
} catch {
|
|
25
63
|
}
|
|
64
|
+
};
|
|
65
|
+
const unsubscribe = detective.subscribe((event) => {
|
|
66
|
+
const mode = detective.config.mode;
|
|
67
|
+
if (mode === "silent") return;
|
|
68
|
+
if (mode === "verbose") {
|
|
69
|
+
try {
|
|
70
|
+
printVerbose(event);
|
|
71
|
+
} catch {
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
pending.push(event);
|
|
76
|
+
if (timer === void 0) {
|
|
77
|
+
timer = setTimeout(flush, BATCH_WINDOW_MS);
|
|
78
|
+
timer.unref?.();
|
|
79
|
+
}
|
|
26
80
|
});
|
|
81
|
+
return () => {
|
|
82
|
+
unsubscribe();
|
|
83
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
84
|
+
};
|
|
27
85
|
}
|
|
28
|
-
function
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
86
|
+
function printBatch(batch, slowThreshold) {
|
|
87
|
+
const byComponent = /* @__PURE__ */ new Map();
|
|
88
|
+
let totalMs = 0;
|
|
89
|
+
let avoidable = 0;
|
|
90
|
+
for (const event of batch) {
|
|
91
|
+
const key = event.component.name;
|
|
92
|
+
const entry = byComponent.get(key) ?? {
|
|
93
|
+
name: key,
|
|
94
|
+
source: event.component.source,
|
|
95
|
+
count: 0,
|
|
96
|
+
totalMs: 0,
|
|
97
|
+
reasons: /* @__PURE__ */ new Map(),
|
|
98
|
+
notableReasons: /* @__PURE__ */ new Map(),
|
|
99
|
+
avoidable: 0,
|
|
100
|
+
remount: false,
|
|
101
|
+
slow: false
|
|
102
|
+
};
|
|
103
|
+
entry.count++;
|
|
104
|
+
entry.totalMs += event.timings.selfDuration;
|
|
105
|
+
entry.reasons.set(event.diagnosis.reason, (entry.reasons.get(event.diagnosis.reason) ?? 0) + 1);
|
|
106
|
+
if (event.diagnosis.potentiallyAvoidable || event.timings.selfDuration >= slowThreshold) {
|
|
107
|
+
entry.notableReasons.set(event.diagnosis.reason, (entry.notableReasons.get(event.diagnosis.reason) ?? 0) + 1);
|
|
108
|
+
}
|
|
109
|
+
if (event.diagnosis.potentiallyAvoidable) entry.avoidable++;
|
|
110
|
+
if (event.diagnosis.summary.includes("rebuilt")) entry.remount = true;
|
|
111
|
+
if (event.timings.selfDuration >= slowThreshold) entry.slow = true;
|
|
112
|
+
if (!entry.worst || event.timings.selfDuration > entry.worst.timings.selfDuration) entry.worst = event;
|
|
113
|
+
byComponent.set(key, entry);
|
|
114
|
+
totalMs += event.timings.selfDuration;
|
|
115
|
+
if (event.diagnosis.potentiallyAvoidable) avoidable++;
|
|
116
|
+
}
|
|
117
|
+
const notable = [...byComponent.values()].filter((a) => a.avoidable > 0 || a.slow || a.remount);
|
|
118
|
+
if (notable.length === 0) return;
|
|
119
|
+
const ranked = [...notable].sort((a, b) => b.totalMs - a.totalMs);
|
|
120
|
+
const anySlow = ranked.some((a) => a.slow);
|
|
121
|
+
const segments = [
|
|
122
|
+
["[RRD] ", "dim"],
|
|
123
|
+
[`${batch.length} render${batch.length === 1 ? "" : "s"}`, "strong"],
|
|
124
|
+
[` \xB7 ${totalMs.toFixed(1)}ms`, "dim"]
|
|
125
|
+
];
|
|
126
|
+
if (avoidable > 0) segments.push([` \xB7 ${avoidable} potentially avoidable`, "warn"]);
|
|
127
|
+
for (const a of ranked.slice(0, 8)) {
|
|
128
|
+
const reasonSource = a.notableReasons.size > 0 ? a.notableReasons : a.reasons;
|
|
129
|
+
const reason = [...reasonSource.entries()].sort((x, y) => y[1] - x[1])[0]?.[0] ?? "unknown";
|
|
130
|
+
const tone = a.remount ? "bad" : a.slow ? "warn" : a.avoidable > 0 ? "warn" : "good";
|
|
131
|
+
const label2 = ` ${ICON[a.slow ? "slow" : "normal"]} ${a.name}${a.count > 1 ? ` \xD7${a.count}` : ""}`;
|
|
132
|
+
segments.push("\n", [label2.padEnd(30), tone], [`${a.totalMs.toFixed(1)}ms`, "plain"], [` ${reason}`, "dim"]);
|
|
133
|
+
if (a.remount) segments.push([" rebuilt", "bad"]);
|
|
134
|
+
if (a.avoidable > 0) segments.push([` ${a.avoidable} avoidable`, "warn"]);
|
|
135
|
+
if (a.source) segments.push(["\n " + a.source, "dim"]);
|
|
136
|
+
}
|
|
137
|
+
const worst = ranked[0]?.worst;
|
|
138
|
+
segments.push(
|
|
139
|
+
"\n",
|
|
140
|
+
[
|
|
141
|
+
worst?.diagnosis.suggestion ? ` \u2192 ${worst.diagnosis.suggestion}` : " \u2192 rrd.printOpportunities() to rank everything by recoverable time",
|
|
142
|
+
"dim"
|
|
143
|
+
]
|
|
144
|
+
);
|
|
145
|
+
const log = anySlow ? console.warn : console.log;
|
|
146
|
+
log(...styled(segments));
|
|
41
147
|
}
|
|
42
148
|
function printVerbose(event) {
|
|
43
149
|
const { diagnosis, timings } = event;
|
|
@@ -60,9 +166,7 @@ function printVerbose(event) {
|
|
|
60
166
|
for (const c of event.changedProps) console.log(propLine(c));
|
|
61
167
|
console.groupEnd();
|
|
62
168
|
}
|
|
63
|
-
if (event.unchangedProps.length > 0) {
|
|
64
|
-
console.log(`Props same: ${event.unchangedProps.join(", ")}`);
|
|
65
|
-
}
|
|
169
|
+
if (event.unchangedProps.length > 0) console.log(`Props same: ${event.unchangedProps.join(", ")}`);
|
|
66
170
|
if (event.trackedState.length > 0) {
|
|
67
171
|
console.log(
|
|
68
172
|
`State: ${event.trackedState.map((s) => `${s.name}: ${formatInspected(s.previous)} \u2192 ${formatInspected(s.current)}`).join(", ")}`
|
|
@@ -113,6 +217,8 @@ function label(event) {
|
|
|
113
217
|
return "context update";
|
|
114
218
|
case "state":
|
|
115
219
|
return "state changed";
|
|
220
|
+
case "store":
|
|
221
|
+
return "store update";
|
|
116
222
|
case "state-or-external":
|
|
117
223
|
return "state or external store";
|
|
118
224
|
case "unknown":
|
|
@@ -120,194 +226,6 @@ function label(event) {
|
|
|
120
226
|
}
|
|
121
227
|
}
|
|
122
228
|
|
|
123
|
-
// src/core/interactions.ts
|
|
124
|
-
var COMMIT_SLACK_MS = 100;
|
|
125
|
-
var FALLBACK_CLOSE_MS = 50;
|
|
126
|
-
var InteractionTracker = class {
|
|
127
|
-
constructor(capacity = 50) {
|
|
128
|
-
this.capacity = capacity;
|
|
129
|
-
this.records = [];
|
|
130
|
-
this.nextId = 0;
|
|
131
|
-
}
|
|
132
|
-
/** Returns false when the browser cannot report event timing. */
|
|
133
|
-
start() {
|
|
134
|
-
if (this.observer) return true;
|
|
135
|
-
const PO = globalThis.PerformanceObserver;
|
|
136
|
-
const supported = PO?.supportedEntryTypes?.includes("event");
|
|
137
|
-
if (!PO || !supported) return false;
|
|
138
|
-
try {
|
|
139
|
-
const observer = new PO((list) => {
|
|
140
|
-
for (const entry of list.getEntries()) {
|
|
141
|
-
const timing = entry;
|
|
142
|
-
this.record({
|
|
143
|
-
name: timing.name,
|
|
144
|
-
startTime: timing.startTime,
|
|
145
|
-
duration: timing.duration,
|
|
146
|
-
target: describeTarget(timing.target)
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
observer.observe({ type: "event", buffered: true, durationThreshold: 16 });
|
|
151
|
-
this.observer = observer;
|
|
152
|
-
return true;
|
|
153
|
-
} catch {
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
stop() {
|
|
158
|
-
this.observer?.disconnect();
|
|
159
|
-
this.observer = void 0;
|
|
160
|
-
}
|
|
161
|
-
/** Is the automatic path available in this browser? */
|
|
162
|
-
get automatic() {
|
|
163
|
-
return this.observer !== void 0;
|
|
164
|
-
}
|
|
165
|
-
/**
|
|
166
|
-
* Time an interaction by hand.
|
|
167
|
-
*
|
|
168
|
-
* The automatic path depends on the Event Timing API, which Safari only
|
|
169
|
-
* gained in 16.4 and which does not fire for synthetic input at all — so
|
|
170
|
-
* anything driven by a test harness records nothing. This measures a specific
|
|
171
|
-
* action instead, up to the paint that follows it, and needs no browser
|
|
172
|
-
* support beyond `performance.now`.
|
|
173
|
-
*/
|
|
174
|
-
measure(label2, action) {
|
|
175
|
-
const startTime = now();
|
|
176
|
-
let handlerMs = 0;
|
|
177
|
-
let finished = false;
|
|
178
|
-
const finish = () => {
|
|
179
|
-
if (finished) return;
|
|
180
|
-
finished = true;
|
|
181
|
-
this.record({ name: label2, startTime, duration: now() - startTime, handlerMs });
|
|
182
|
-
};
|
|
183
|
-
let result;
|
|
184
|
-
try {
|
|
185
|
-
result = action();
|
|
186
|
-
handlerMs = now() - startTime;
|
|
187
|
-
} catch (error) {
|
|
188
|
-
handlerMs = now() - startTime;
|
|
189
|
-
finish();
|
|
190
|
-
throw error;
|
|
191
|
-
}
|
|
192
|
-
const raf = globalThis.requestAnimationFrame;
|
|
193
|
-
if (raf) raf(() => raf(finish));
|
|
194
|
-
setTimeout(finish, FALLBACK_CLOSE_MS);
|
|
195
|
-
return result;
|
|
196
|
-
}
|
|
197
|
-
/** Exposed for tests and for `measure`. */
|
|
198
|
-
record(timing) {
|
|
199
|
-
const record = {
|
|
200
|
-
id: `interaction_${++this.nextId}`,
|
|
201
|
-
type: timing.name,
|
|
202
|
-
target: timing.target,
|
|
203
|
-
startTime: timing.startTime,
|
|
204
|
-
durationMs: timing.duration,
|
|
205
|
-
handlerMs: timing.handlerMs,
|
|
206
|
-
renders: [],
|
|
207
|
-
renderTimeMs: 0,
|
|
208
|
-
avoidableRenderTimeMs: 0
|
|
209
|
-
};
|
|
210
|
-
this.records.push(record);
|
|
211
|
-
if (this.records.length > this.capacity) this.records.shift();
|
|
212
|
-
return record;
|
|
213
|
-
}
|
|
214
|
-
clear() {
|
|
215
|
-
this.records.length = 0;
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* Joins render events to interactions by commit time. A render belongs to an
|
|
219
|
-
* interaction when it committed between the event starting and shortly after
|
|
220
|
-
* it finished — React commits just after the event handler returns.
|
|
221
|
-
*/
|
|
222
|
-
attribute(events) {
|
|
223
|
-
for (const record of this.records) {
|
|
224
|
-
const from = record.startTime;
|
|
225
|
-
const to = record.startTime + record.durationMs + COMMIT_SLACK_MS;
|
|
226
|
-
record.renders = events.filter((e) => e.timings.commitTime >= from && e.timings.commitTime <= to);
|
|
227
|
-
record.renderTimeMs = record.renders.reduce((a, e) => a + e.timings.selfDuration, 0);
|
|
228
|
-
record.avoidableRenderTimeMs = record.renders.filter((e) => e.diagnosis.potentiallyAvoidable).reduce((a, e) => a + e.timings.selfDuration, 0);
|
|
229
|
-
}
|
|
230
|
-
return [...this.records].sort((a, b) => b.durationMs - a.durationMs);
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
function summarise(record) {
|
|
234
|
-
const byComponent = /* @__PURE__ */ new Map();
|
|
235
|
-
for (const event of record.renders) {
|
|
236
|
-
const entry = byComponent.get(event.component.name) ?? {
|
|
237
|
-
renders: 0,
|
|
238
|
-
totalMs: 0,
|
|
239
|
-
source: event.component.source,
|
|
240
|
-
cause: event.diagnosis.reason
|
|
241
|
-
};
|
|
242
|
-
entry.renders++;
|
|
243
|
-
entry.totalMs += event.timings.selfDuration;
|
|
244
|
-
byComponent.set(event.component.name, entry);
|
|
245
|
-
}
|
|
246
|
-
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);
|
|
247
|
-
const top = contributors[0];
|
|
248
|
-
const accounted = (record.handlerMs ?? record.durationMs) + record.renderTimeMs;
|
|
249
|
-
const idleWindow = record.handlerMs !== void 0 && record.durationMs > accounted * 3 && record.durationMs - accounted > 100;
|
|
250
|
-
const effectiveMs = idleWindow ? accounted : record.durationMs;
|
|
251
|
-
const share = effectiveMs > 0 ? record.renderTimeMs / effectiveMs : 0;
|
|
252
|
-
let headline;
|
|
253
|
-
let nextStep;
|
|
254
|
-
let confidence = "medium";
|
|
255
|
-
if (idleWindow) {
|
|
256
|
-
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.`;
|
|
257
|
-
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.";
|
|
258
|
-
return { interaction: record, contributors, headline, nextStep, confidence: "medium" };
|
|
259
|
-
}
|
|
260
|
-
if (record.renders.length === 0) {
|
|
261
|
-
headline = `${record.type} took ${fmt(record.durationMs)}, and no instrumented component rendered inside it.`;
|
|
262
|
-
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.";
|
|
263
|
-
confidence = "low";
|
|
264
|
-
} else if (share >= 0.4 && record.avoidableRenderTimeMs > 0) {
|
|
265
|
-
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.`;
|
|
266
|
-
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.";
|
|
267
|
-
confidence = "high";
|
|
268
|
-
} else if (share >= 0.4) {
|
|
269
|
-
headline = `${record.type} took ${fmt(record.durationMs)}; ${fmt(record.renderTimeMs)} of it was rendering, all of it explained by real input changes.`;
|
|
270
|
-
nextStep = "This is genuine work. Make the renders cheaper rather than fewer \u2014 or do less of it per interaction.";
|
|
271
|
-
confidence = "high";
|
|
272
|
-
} else {
|
|
273
|
-
headline = `${record.type} took ${fmt(record.durationMs)}, but only ${fmt(record.renderTimeMs)} was React rendering.`;
|
|
274
|
-
nextStep = "Most of the cost is outside rendering \u2014 event handlers, layout or paint. A browser profile will show more than this tool can.";
|
|
275
|
-
confidence = "medium";
|
|
276
|
-
}
|
|
277
|
-
return { interaction: record, contributors, headline, nextStep, confidence };
|
|
278
|
-
}
|
|
279
|
-
function formatInteraction(summary) {
|
|
280
|
-
const { interaction: i } = summary;
|
|
281
|
-
const lines = [
|
|
282
|
-
`${i.type}${i.target ? ` on ${i.target}` : ""} ${fmt(i.durationMs)}`,
|
|
283
|
-
"",
|
|
284
|
-
summary.headline
|
|
285
|
-
];
|
|
286
|
-
if (summary.contributors.length > 0) {
|
|
287
|
-
lines.push("", "Rendering inside this interaction");
|
|
288
|
-
for (const c of summary.contributors.slice(0, 8)) {
|
|
289
|
-
lines.push(
|
|
290
|
-
` ${c.component.padEnd(22)} ${String(c.renders).padStart(4)} render(s) ${fmt(c.totalMs).padStart(8)} ${c.cause}`
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
lines.push("", `Next step`, ` ${summary.nextStep}`, "", `Confidence: ${summary.confidence}`);
|
|
295
|
-
return lines.join("\n");
|
|
296
|
-
}
|
|
297
|
-
function describeTarget(target) {
|
|
298
|
-
if (!target || typeof target !== "object") return void 0;
|
|
299
|
-
const el = target;
|
|
300
|
-
if (!el.tagName) return void 0;
|
|
301
|
-
const tag = el.tagName.toLowerCase();
|
|
302
|
-
if (el.id) return `${tag}#${el.id}`;
|
|
303
|
-
const className = typeof el.className === "string" ? el.className.trim().split(/\s+/)[0] : void 0;
|
|
304
|
-
if (className) return `${tag}.${className}`;
|
|
305
|
-
const text = el.textContent?.trim().slice(0, 20);
|
|
306
|
-
return text ? `${tag} "${text}"` : tag;
|
|
307
|
-
}
|
|
308
|
-
var fmt = (ms) => `${ms.toFixed(1)}ms`;
|
|
309
|
-
var now = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
310
|
-
|
|
311
229
|
// src/core/opportunities.ts
|
|
312
230
|
var DEFAULT_MIN_SAVING_MS = 1;
|
|
313
231
|
function rankOpportunities({ events, lifecycles, minSavingMs = DEFAULT_MIN_SAVING_MS }) {
|
|
@@ -426,6 +344,38 @@ function RenderDetective({ name, children }) {
|
|
|
426
344
|
const { node, onRender } = useInstrumentedNode(name, { children });
|
|
427
345
|
return renderInstrumented(node, onRender, children);
|
|
428
346
|
}
|
|
347
|
+
var useOwnerId = typeof React.useId === "function" ? React.useId : () => "";
|
|
348
|
+
function trackSelector(value, options = {}) {
|
|
349
|
+
const detective = getDetective();
|
|
350
|
+
const node = useContext(AncestryContext);
|
|
351
|
+
const ownerId = useOwnerId();
|
|
352
|
+
const previous = useRef(void 0);
|
|
353
|
+
if (!detective.enabled) return value;
|
|
354
|
+
if (node && node.stateOwner === void 0) node.stateOwner = ownerId;
|
|
355
|
+
const owns = !node || node.stateOwner === ownerId;
|
|
356
|
+
const prior = previous.current;
|
|
357
|
+
previous.current = { value };
|
|
358
|
+
if (!prior || !node || !owns) return value;
|
|
359
|
+
if (Object.is(prior.value, value)) return value;
|
|
360
|
+
try {
|
|
361
|
+
const equal = shallowEqual(prior.value, value, detective.config);
|
|
362
|
+
detective.recordSelectorChange(node, {
|
|
363
|
+
name: options.name ?? options.source ?? "selector",
|
|
364
|
+
source: options.source,
|
|
365
|
+
referenceOnly: equal === true,
|
|
366
|
+
previous: inspect(prior.value, detective.config.inspection),
|
|
367
|
+
current: inspect(value, detective.config.inspection)
|
|
368
|
+
});
|
|
369
|
+
} catch {
|
|
370
|
+
}
|
|
371
|
+
return value;
|
|
372
|
+
}
|
|
373
|
+
function createTrackedSelectorHook(hook, options = {}) {
|
|
374
|
+
return function useTrackedSelector(...args) {
|
|
375
|
+
const value = hook(...args);
|
|
376
|
+
return trackSelector(value, options);
|
|
377
|
+
};
|
|
378
|
+
}
|
|
429
379
|
function useRenderDiagnostics(name, props) {
|
|
430
380
|
const detective = getDetective();
|
|
431
381
|
const parent = useContext(AncestryContext);
|
|
@@ -460,11 +410,11 @@ function useRenderDiagnostics(name, props) {
|
|
|
460
410
|
}
|
|
461
411
|
return last.current;
|
|
462
412
|
}
|
|
463
|
-
var
|
|
413
|
+
var useOwnerId2 = typeof React.useId === "function" ? React.useId : () => "";
|
|
464
414
|
function useTrackedState(name, initial) {
|
|
465
415
|
const detective = getDetective();
|
|
466
416
|
const node = useContext(AncestryContext);
|
|
467
|
-
const ownerId =
|
|
417
|
+
const ownerId = useOwnerId2();
|
|
468
418
|
const [state, setState] = useState(initial);
|
|
469
419
|
const previous = useRef(state);
|
|
470
420
|
if (node && node.stateOwner === void 0) node.stateOwner = ownerId;
|
|
@@ -525,21 +475,12 @@ function isRecord(v) {
|
|
|
525
475
|
|
|
526
476
|
// src/index.ts
|
|
527
477
|
var REPORTER = /* @__PURE__ */ Symbol.for("react-render-detective.reporter");
|
|
528
|
-
var INTERACTIONS = /* @__PURE__ */ Symbol.for("react-render-detective.interactions");
|
|
529
|
-
function tracker() {
|
|
530
|
-
const g = globalThis;
|
|
531
|
-
if (!g[INTERACTIONS]) g[INTERACTIONS] = new InteractionTracker();
|
|
532
|
-
return g[INTERACTIONS];
|
|
533
|
-
}
|
|
534
478
|
function init(options = {}) {
|
|
535
479
|
const detective = getDetective();
|
|
536
480
|
detective.init(options);
|
|
537
481
|
const g = globalThis;
|
|
538
482
|
g[REPORTER]?.();
|
|
539
483
|
g[REPORTER] = void 0;
|
|
540
|
-
if (detective.enabled) {
|
|
541
|
-
tracker().start();
|
|
542
|
-
}
|
|
543
484
|
if (detective.enabled && detective.config.mode !== "silent") {
|
|
544
485
|
g[REPORTER] = attachConsoleReporter(detective);
|
|
545
486
|
} else if (!detective.enabled) {
|
|
@@ -571,14 +512,11 @@ function subscribe(listener) {
|
|
|
571
512
|
}
|
|
572
513
|
function clear() {
|
|
573
514
|
getDetective().clear();
|
|
574
|
-
tracker().clear();
|
|
575
515
|
}
|
|
576
516
|
function reset() {
|
|
577
517
|
const g = globalThis;
|
|
578
518
|
g[REPORTER]?.();
|
|
579
519
|
g[REPORTER] = void 0;
|
|
580
|
-
g[INTERACTIONS]?.stop();
|
|
581
|
-
g[INTERACTIONS] = void 0;
|
|
582
520
|
getDetective().reset();
|
|
583
521
|
}
|
|
584
522
|
function explain(componentName2) {
|
|
@@ -593,31 +531,6 @@ function getRenderProfile(scenario) {
|
|
|
593
531
|
for (const stats of getDetective().getComponentStats()) remounts[stats.name] = stats.remountCount;
|
|
594
532
|
return profileFromEvents(scenario, getEvents(), remounts);
|
|
595
533
|
}
|
|
596
|
-
function getInteractions() {
|
|
597
|
-
return tracker().attribute(getEvents());
|
|
598
|
-
}
|
|
599
|
-
function explainInteractionStructured(id) {
|
|
600
|
-
const records = getInteractions();
|
|
601
|
-
const record = id ? records.find((r) => r.id === id) : records[0];
|
|
602
|
-
return record ? summarise(record) : void 0;
|
|
603
|
-
}
|
|
604
|
-
function explainInteraction(id) {
|
|
605
|
-
const summary = explainInteractionStructured(id);
|
|
606
|
-
return summary ? formatInteraction(summary) : void 0;
|
|
607
|
-
}
|
|
608
|
-
function printInteractions(limit = 5) {
|
|
609
|
-
const records = getInteractions().slice(0, limit);
|
|
610
|
-
if (records.length === 0) {
|
|
611
|
-
console.log(
|
|
612
|
-
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."
|
|
613
|
-
);
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
console.log(records.map((r) => formatInteraction(summarise(r))).join("\n\n"));
|
|
617
|
-
}
|
|
618
|
-
function measureInteraction(label2, action) {
|
|
619
|
-
return tracker().measure(label2, action);
|
|
620
|
-
}
|
|
621
534
|
function getOpportunities(limit = 10) {
|
|
622
535
|
const detective = getDetective();
|
|
623
536
|
const lifecycles = /* @__PURE__ */ new Map();
|
|
@@ -676,15 +589,10 @@ var ReactRenderDetective = {
|
|
|
676
589
|
explainStructured,
|
|
677
590
|
getOpportunities,
|
|
678
591
|
printOpportunities,
|
|
679
|
-
getInteractions,
|
|
680
|
-
explainInteraction,
|
|
681
|
-
explainInteractionStructured,
|
|
682
|
-
printInteractions,
|
|
683
|
-
measureInteraction,
|
|
684
592
|
getRenderProfile,
|
|
685
593
|
printStats
|
|
686
594
|
};
|
|
687
595
|
|
|
688
|
-
export {
|
|
596
|
+
export { ReactRenderDetective, RenderDetective, clear, configure, createTrackedSelectorHook, explain, explainStructured, formatOpportunities, getComponentStats, getConfig, getEvents, getOpportunities, getRenderProfile, getStats, init, isEnabled, printOpportunities, printStats, rankOpportunities, reset, subscribe, trackSelector, useRenderDiagnostics, useTrackedContextValue, useTrackedEffect, useTrackedState, withRenderDetective };
|
|
689
597
|
//# sourceMappingURL=index.js.map
|
|
690
598
|
//# sourceMappingURL=index.js.map
|