react-render-detective 0.2.0 → 0.4.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/README.md +1 -1
- package/dist/babel.cjs +2 -2
- package/dist/babel.js +1 -1
- package/dist/{chunk-6YPJQ7M5.cjs → chunk-DZ3BZ654.cjs} +88 -4
- package/dist/chunk-DZ3BZ654.cjs.map +1 -0
- package/dist/chunk-H5RG2EPP.cjs +75 -0
- package/dist/chunk-H5RG2EPP.cjs.map +1 -0
- package/dist/{chunk-LSIXHWQD.js → chunk-HOTY7J3X.js} +14 -4
- package/dist/chunk-HOTY7J3X.js.map +1 -0
- package/dist/{chunk-QQ4VARSD.cjs → chunk-JD4IJ4MN.cjs} +14 -4
- package/dist/chunk-JD4IJ4MN.cjs.map +1 -0
- package/dist/chunk-LILK23YH.js +71 -0
- package/dist/chunk-LILK23YH.js.map +1 -0
- package/dist/{chunk-PSCFSJ65.js → chunk-MCIQMYNR.js} +12 -5
- package/dist/chunk-MCIQMYNR.js.map +1 -0
- package/dist/{chunk-BPNLNIHU.cjs → chunk-PHYYA67T.cjs} +12 -5
- package/dist/chunk-PHYYA67T.cjs.map +1 -0
- package/dist/{chunk-MSISKPST.js → chunk-QOGTEVBP.js} +88 -4
- package/dist/chunk-QOGTEVBP.js.map +1 -0
- package/dist/core.cjs +18 -18
- package/dist/core.d.cts +38 -2
- package/dist/core.d.ts +38 -2
- package/dist/core.js +1 -1
- package/dist/index.cjs +354 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +172 -4
- package/dist/index.d.ts +172 -4
- package/dist/index.js +316 -6
- package/dist/index.js.map +1 -1
- package/dist/overlay.cjs +6 -6
- package/dist/overlay.cjs.map +1 -1
- package/dist/overlay.js +4 -4
- package/dist/overlay.js.map +1 -1
- package/dist/testing.cjs +20 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +62 -0
- package/dist/testing.d.ts +62 -0
- package/dist/testing.js +3 -0
- package/dist/testing.js.map +1 -0
- package/dist/{types-BWVaQPHb.d.cts → types-gl13xwmN.d.cts} +5 -0
- package/dist/{types-BWVaQPHb.d.ts → types-gl13xwmN.d.ts} +5 -0
- package/dist/vite.cjs +9 -6
- package/dist/vite.cjs.map +1 -1
- package/dist/vite.js +8 -5
- package/dist/vite.js.map +1 -1
- package/package.json +6 -1
- package/dist/chunk-6YPJQ7M5.cjs.map +0 -1
- package/dist/chunk-BPNLNIHU.cjs.map +0 -1
- package/dist/chunk-LSIXHWQD.js.map +0 -1
- package/dist/chunk-MSISKPST.js.map +0 -1
- package/dist/chunk-PSCFSJ65.js.map +0 -1
- package/dist/chunk-QQ4VARSD.cjs.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
4
|
-
var
|
|
3
|
+
var chunkJD4IJ4MN_cjs = require('./chunk-JD4IJ4MN.cjs');
|
|
4
|
+
var chunkDZ3BZ654_cjs = require('./chunk-DZ3BZ654.cjs');
|
|
5
|
+
var chunkH5RG2EPP_cjs = require('./chunk-H5RG2EPP.cjs');
|
|
5
6
|
var React = require('react');
|
|
6
7
|
var jsxRuntime = require('react/jsx-runtime');
|
|
7
8
|
|
|
@@ -84,7 +85,7 @@ function printVerbose(event) {
|
|
|
84
85
|
}
|
|
85
86
|
if (event.trackedState.length > 0) {
|
|
86
87
|
console.log(
|
|
87
|
-
`State: ${event.trackedState.map((s) => `${s.name}: ${
|
|
88
|
+
`State: ${event.trackedState.map((s) => `${s.name}: ${chunkDZ3BZ654_cjs.formatInspected(s.previous)} \u2192 ${chunkDZ3BZ654_cjs.formatInspected(s.current)}`).join(", ")}`
|
|
88
89
|
);
|
|
89
90
|
}
|
|
90
91
|
if (event.contextChanges.length > 0) {
|
|
@@ -101,11 +102,11 @@ function printVerbose(event) {
|
|
|
101
102
|
}
|
|
102
103
|
function propLine(c) {
|
|
103
104
|
const head = `${c.key} (${c.valueType}) \u2014 ${describeKind(c)}`;
|
|
104
|
-
if (c.kind === "added") return `${head}: ${
|
|
105
|
-
if (c.kind === "removed") return `${head}: was ${
|
|
105
|
+
if (c.kind === "added") return `${head}: ${chunkDZ3BZ654_cjs.formatInspected(c.current)}`;
|
|
106
|
+
if (c.kind === "removed") return `${head}: was ${chunkDZ3BZ654_cjs.formatInspected(c.previous)}`;
|
|
106
107
|
return `${head}
|
|
107
|
-
previous: ${
|
|
108
|
-
current: ${
|
|
108
|
+
previous: ${chunkDZ3BZ654_cjs.formatInspected(c.previous)}
|
|
109
|
+
current: ${chunkDZ3BZ654_cjs.formatInspected(c.current)}`;
|
|
109
110
|
}
|
|
110
111
|
function describeKind(c) {
|
|
111
112
|
switch (c.kind) {
|
|
@@ -138,10 +139,249 @@ function label(event) {
|
|
|
138
139
|
return "undetermined";
|
|
139
140
|
}
|
|
140
141
|
}
|
|
142
|
+
|
|
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
|
+
// src/core/opportunities.ts
|
|
332
|
+
var DEFAULT_MIN_SAVING_MS = 1;
|
|
333
|
+
function rankOpportunities({ events, lifecycles, minSavingMs = DEFAULT_MIN_SAVING_MS }) {
|
|
334
|
+
const names = new Set(events.map((e) => e.component.name));
|
|
335
|
+
const out = [];
|
|
336
|
+
for (const name of names) {
|
|
337
|
+
const lifecycle = lifecycles.get(name);
|
|
338
|
+
const explanation = chunkJD4IJ4MN_cjs.explainEvents(name, events, lifecycle);
|
|
339
|
+
if (!explanation) continue;
|
|
340
|
+
const mounts = events.filter((e) => e.component.name === name && e.phase === "mount");
|
|
341
|
+
const averageMountCost = mounts.length ? mounts.reduce((a, e) => a + e.timings.selfDuration, 0) / mounts.length : 0;
|
|
342
|
+
const remountSaving = explanation.remounts * averageMountCost;
|
|
343
|
+
const estimatedSavingMs = explanation.estimatedAvoidableTime + remountSaving;
|
|
344
|
+
if (estimatedSavingMs < minSavingMs) continue;
|
|
345
|
+
out.push({
|
|
346
|
+
component: name,
|
|
347
|
+
source: explanation.source,
|
|
348
|
+
estimatedSavingMs,
|
|
349
|
+
avoidableRenders: explanation.potentiallyAvoidableRenders,
|
|
350
|
+
remounts: explanation.remounts,
|
|
351
|
+
averageSelfDuration: explanation.averageSelfDuration,
|
|
352
|
+
summary: explanation.headline,
|
|
353
|
+
nextStep: explanation.nextStep,
|
|
354
|
+
confidence: explanation.confidence
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
return out.sort((a, b) => b.estimatedSavingMs - a.estimatedSavingMs);
|
|
358
|
+
}
|
|
359
|
+
function formatOpportunities(opportunities) {
|
|
360
|
+
if (opportunities.length === 0) {
|
|
361
|
+
return "React Render Detective\n\nNo measurable render waste found yet. Interact with the app and try again.";
|
|
362
|
+
}
|
|
363
|
+
const lines = [
|
|
364
|
+
"React Render Detective \u2014 where to spend your next hour",
|
|
365
|
+
"",
|
|
366
|
+
"Ranked by estimated recoverable time. These are estimates, not promises:",
|
|
367
|
+
"measure each fix.",
|
|
368
|
+
""
|
|
369
|
+
];
|
|
370
|
+
for (const [index, o] of opportunities.entries()) {
|
|
371
|
+
lines.push(
|
|
372
|
+
`${String(index + 1).padStart(2)}. ${o.component}${o.source ? ` ${o.source}` : ""}`,
|
|
373
|
+
` ~${o.estimatedSavingMs.toFixed(0)}ms recoverable ${o.avoidableRenders} avoidable render${o.avoidableRenders === 1 ? "" : "s"}${o.remounts > 0 ? `, ${o.remounts}\xD7 rebuilt` : ""} (confidence: ${o.confidence})`,
|
|
374
|
+
` ${o.summary}`,
|
|
375
|
+
` \u2192 ${o.nextStep}`,
|
|
376
|
+
""
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
return lines.join("\n");
|
|
380
|
+
}
|
|
141
381
|
var AncestryContext = React.createContext(void 0);
|
|
142
382
|
AncestryContext.displayName = "RenderDetectiveAncestry";
|
|
143
383
|
function useInstrumentedNode(name, props, source) {
|
|
144
|
-
const detective =
|
|
384
|
+
const detective = chunkDZ3BZ654_cjs.getDetective();
|
|
145
385
|
const parent = React.useContext(AncestryContext);
|
|
146
386
|
const [node] = React.useState(
|
|
147
387
|
() => detective.enabled ? detective.createNode(name, parent, source) : void 0
|
|
@@ -191,6 +431,7 @@ var IS_WRAPPER = /* @__PURE__ */ Symbol.for("react-render-detective.wrapper");
|
|
|
191
431
|
function withRenderDetective(Component, options = {}) {
|
|
192
432
|
if (Component[IS_WRAPPER]) return Component;
|
|
193
433
|
const name = options.name ?? componentName(Component);
|
|
434
|
+
chunkDZ3BZ654_cjs.getDetective().noteDefinition(name, options.source, options.declaredInRender === true);
|
|
194
435
|
function RenderDetected(props) {
|
|
195
436
|
const { node, onRender } = useInstrumentedNode(name, props, options.source);
|
|
196
437
|
return renderInstrumented(node, onRender, /* @__PURE__ */ jsxRuntime.jsx(Component, { ...props }));
|
|
@@ -206,7 +447,7 @@ function RenderDetective({ name, children }) {
|
|
|
206
447
|
return renderInstrumented(node, onRender, children);
|
|
207
448
|
}
|
|
208
449
|
function useRenderDiagnostics(name, props) {
|
|
209
|
-
const detective =
|
|
450
|
+
const detective = chunkDZ3BZ654_cjs.getDetective();
|
|
210
451
|
const parent = React.useContext(AncestryContext);
|
|
211
452
|
const [node] = React.useState(() => {
|
|
212
453
|
if (!detective.enabled) return void 0;
|
|
@@ -241,7 +482,7 @@ function useRenderDiagnostics(name, props) {
|
|
|
241
482
|
}
|
|
242
483
|
var useOwnerId = typeof React__namespace.useId === "function" ? React__namespace.useId : () => "";
|
|
243
484
|
function useTrackedState(name, initial) {
|
|
244
|
-
const detective =
|
|
485
|
+
const detective = chunkDZ3BZ654_cjs.getDetective();
|
|
245
486
|
const node = React.useContext(AncestryContext);
|
|
246
487
|
const ownerId = useOwnerId();
|
|
247
488
|
const [state, setState] = React.useState(initial);
|
|
@@ -251,8 +492,8 @@ function useTrackedState(name, initial) {
|
|
|
251
492
|
if (node && owns && !Object.is(previous.current, state)) {
|
|
252
493
|
detective.recordStateChange(node, {
|
|
253
494
|
name,
|
|
254
|
-
previous:
|
|
255
|
-
current:
|
|
495
|
+
previous: chunkDZ3BZ654_cjs.inspect(previous.current, detective.config.inspection),
|
|
496
|
+
current: chunkDZ3BZ654_cjs.inspect(state, detective.config.inspection)
|
|
256
497
|
});
|
|
257
498
|
previous.current = state;
|
|
258
499
|
} else if (!owns) {
|
|
@@ -261,7 +502,7 @@ function useTrackedState(name, initial) {
|
|
|
261
502
|
return [state, setState];
|
|
262
503
|
}
|
|
263
504
|
function useTrackedEffect(name, effect, deps) {
|
|
264
|
-
const detective =
|
|
505
|
+
const detective = chunkDZ3BZ654_cjs.getDetective();
|
|
265
506
|
const previous = React.useRef(void 0);
|
|
266
507
|
const changed = React.useRef([]);
|
|
267
508
|
if (detective.enabled) {
|
|
@@ -277,7 +518,7 @@ function useTrackedEffect(name, effect, deps) {
|
|
|
277
518
|
}, deps);
|
|
278
519
|
}
|
|
279
520
|
function useTrackedContextValue(contextName, value) {
|
|
280
|
-
const detective =
|
|
521
|
+
const detective = chunkDZ3BZ654_cjs.getDetective();
|
|
281
522
|
const previous = React.useRef(void 0);
|
|
282
523
|
const first = React.useRef(true);
|
|
283
524
|
if (detective.enabled) {
|
|
@@ -285,8 +526,8 @@ function useTrackedContextValue(contextName, value) {
|
|
|
285
526
|
first.current = false;
|
|
286
527
|
} else if (!Object.is(previous.current, value)) {
|
|
287
528
|
const prev = previous.current;
|
|
288
|
-
const equal =
|
|
289
|
-
const changedKeys = isRecord(prev) && isRecord(value) ?
|
|
529
|
+
const equal = chunkDZ3BZ654_cjs.shallowEqual(prev, value, detective.config);
|
|
530
|
+
const changedKeys = isRecord(prev) && isRecord(value) ? chunkDZ3BZ654_cjs.diffProps(prev, value, detective.config).changed.map((c) => c.key) : [];
|
|
290
531
|
detective.recordContextChange({
|
|
291
532
|
contextName,
|
|
292
533
|
changedKeys,
|
|
@@ -304,12 +545,21 @@ function isRecord(v) {
|
|
|
304
545
|
|
|
305
546
|
// src/index.ts
|
|
306
547
|
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
|
+
}
|
|
307
554
|
function init(options = {}) {
|
|
308
|
-
const detective =
|
|
555
|
+
const detective = chunkDZ3BZ654_cjs.getDetective();
|
|
309
556
|
detective.init(options);
|
|
310
557
|
const g = globalThis;
|
|
311
558
|
g[REPORTER]?.();
|
|
312
559
|
g[REPORTER] = void 0;
|
|
560
|
+
if (detective.enabled) {
|
|
561
|
+
tracker().start();
|
|
562
|
+
}
|
|
313
563
|
if (detective.enabled && detective.config.mode !== "silent") {
|
|
314
564
|
g[REPORTER] = attachConsoleReporter(detective);
|
|
315
565
|
} else if (!detective.enabled) {
|
|
@@ -319,41 +569,85 @@ function init(options = {}) {
|
|
|
319
569
|
}
|
|
320
570
|
}
|
|
321
571
|
function configure(options) {
|
|
322
|
-
|
|
572
|
+
chunkDZ3BZ654_cjs.getDetective().configure(options);
|
|
323
573
|
}
|
|
324
574
|
function getConfig() {
|
|
325
|
-
return
|
|
575
|
+
return chunkDZ3BZ654_cjs.getDetective().config;
|
|
326
576
|
}
|
|
327
577
|
function isEnabled() {
|
|
328
|
-
return
|
|
578
|
+
return chunkDZ3BZ654_cjs.getDetective().enabled;
|
|
329
579
|
}
|
|
330
580
|
function getEvents() {
|
|
331
|
-
return
|
|
581
|
+
return chunkDZ3BZ654_cjs.getDetective().getEvents();
|
|
332
582
|
}
|
|
333
583
|
function getStats() {
|
|
334
|
-
return
|
|
584
|
+
return chunkDZ3BZ654_cjs.getDetective().getStats();
|
|
335
585
|
}
|
|
336
586
|
function getComponentStats(name) {
|
|
337
|
-
return
|
|
587
|
+
return chunkDZ3BZ654_cjs.getDetective().getComponentStats(name);
|
|
338
588
|
}
|
|
339
589
|
function subscribe(listener) {
|
|
340
|
-
return
|
|
590
|
+
return chunkDZ3BZ654_cjs.getDetective().subscribe(listener);
|
|
341
591
|
}
|
|
342
592
|
function clear() {
|
|
343
|
-
|
|
593
|
+
chunkDZ3BZ654_cjs.getDetective().clear();
|
|
594
|
+
tracker().clear();
|
|
344
595
|
}
|
|
345
596
|
function reset() {
|
|
346
597
|
const g = globalThis;
|
|
347
598
|
g[REPORTER]?.();
|
|
348
599
|
g[REPORTER] = void 0;
|
|
349
|
-
|
|
600
|
+
g[INTERACTIONS]?.stop();
|
|
601
|
+
g[INTERACTIONS] = void 0;
|
|
602
|
+
chunkDZ3BZ654_cjs.getDetective().reset();
|
|
350
603
|
}
|
|
351
604
|
function explain(componentName2) {
|
|
352
|
-
const explanation =
|
|
353
|
-
return explanation ?
|
|
605
|
+
const explanation = explainStructured(componentName2);
|
|
606
|
+
return explanation ? chunkJD4IJ4MN_cjs.formatExplanation(explanation) : void 0;
|
|
354
607
|
}
|
|
355
608
|
function explainStructured(componentName2) {
|
|
356
|
-
return
|
|
609
|
+
return chunkJD4IJ4MN_cjs.explainEvents(componentName2, getEvents(), chunkDZ3BZ654_cjs.getDetective().lifecycleOf(componentName2));
|
|
610
|
+
}
|
|
611
|
+
function getRenderProfile(scenario) {
|
|
612
|
+
const remounts = {};
|
|
613
|
+
for (const stats of chunkDZ3BZ654_cjs.getDetective().getComponentStats()) remounts[stats.name] = stats.remountCount;
|
|
614
|
+
return chunkH5RG2EPP_cjs.profileFromEvents(scenario, getEvents(), remounts);
|
|
615
|
+
}
|
|
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
|
+
function getOpportunities(limit = 10) {
|
|
642
|
+
const detective = chunkDZ3BZ654_cjs.getDetective();
|
|
643
|
+
const lifecycles = /* @__PURE__ */ new Map();
|
|
644
|
+
for (const stats of detective.getComponentStats()) {
|
|
645
|
+
lifecycles.set(stats.name, { remounts: stats.remountCount });
|
|
646
|
+
}
|
|
647
|
+
return rankOpportunities({ events: detective.getEvents(), lifecycles }).slice(0, limit);
|
|
648
|
+
}
|
|
649
|
+
function printOpportunities(limit = 10) {
|
|
650
|
+
console.log(formatOpportunities(getOpportunities(limit)));
|
|
357
651
|
}
|
|
358
652
|
function printStats() {
|
|
359
653
|
const s = getStats();
|
|
@@ -366,6 +660,14 @@ function printStats() {
|
|
|
366
660
|
`Slow renders ${s.slowRenders}`,
|
|
367
661
|
`Potentially avoidable ${s.potentiallyAvoidableRenders}`
|
|
368
662
|
];
|
|
663
|
+
const rebuilt = s.mostRendered.filter((c) => c.remountCount >= 2);
|
|
664
|
+
if (rebuilt.length > 0) {
|
|
665
|
+
lines.push(
|
|
666
|
+
"",
|
|
667
|
+
"Rebuilt rather than re-rendered (state and DOM discarded each time)",
|
|
668
|
+
...rebuilt.slice(0, 5).map((c) => ` ${c.name.padEnd(22)} ${String(c.remountCount).padStart(5)}\xD7 remounted`)
|
|
669
|
+
);
|
|
670
|
+
}
|
|
369
671
|
if (s.devReplays > 0) {
|
|
370
672
|
lines.push(`Development replays ${s.devReplays} (StrictMode / discarded \u2014 not counted above)`);
|
|
371
673
|
}
|
|
@@ -392,32 +694,53 @@ var ReactRenderDetective = {
|
|
|
392
694
|
reset,
|
|
393
695
|
explain,
|
|
394
696
|
explainStructured,
|
|
697
|
+
getOpportunities,
|
|
698
|
+
printOpportunities,
|
|
699
|
+
getInteractions,
|
|
700
|
+
explainInteraction,
|
|
701
|
+
explainInteractionStructured,
|
|
702
|
+
printInteractions,
|
|
703
|
+
measureInteraction,
|
|
704
|
+
getRenderProfile,
|
|
395
705
|
printStats
|
|
396
706
|
};
|
|
397
707
|
|
|
398
708
|
Object.defineProperty(exports, "explainEvents", {
|
|
399
709
|
enumerable: true,
|
|
400
|
-
get: function () { return
|
|
710
|
+
get: function () { return chunkJD4IJ4MN_cjs.explainEvents; }
|
|
401
711
|
});
|
|
402
712
|
Object.defineProperty(exports, "formatExplanation", {
|
|
403
713
|
enumerable: true,
|
|
404
|
-
get: function () { return
|
|
714
|
+
get: function () { return chunkJD4IJ4MN_cjs.formatExplanation; }
|
|
405
715
|
});
|
|
716
|
+
exports.InteractionTracker = InteractionTracker;
|
|
406
717
|
exports.ReactRenderDetective = ReactRenderDetective;
|
|
407
718
|
exports.RenderDetective = RenderDetective;
|
|
408
719
|
exports.clear = clear;
|
|
409
720
|
exports.configure = configure;
|
|
410
721
|
exports.explain = explain;
|
|
722
|
+
exports.explainInteraction = explainInteraction;
|
|
723
|
+
exports.explainInteractionStructured = explainInteractionStructured;
|
|
411
724
|
exports.explainStructured = explainStructured;
|
|
725
|
+
exports.formatInteraction = formatInteraction;
|
|
726
|
+
exports.formatOpportunities = formatOpportunities;
|
|
412
727
|
exports.getComponentStats = getComponentStats;
|
|
413
728
|
exports.getConfig = getConfig;
|
|
414
729
|
exports.getEvents = getEvents;
|
|
730
|
+
exports.getInteractions = getInteractions;
|
|
731
|
+
exports.getOpportunities = getOpportunities;
|
|
732
|
+
exports.getRenderProfile = getRenderProfile;
|
|
415
733
|
exports.getStats = getStats;
|
|
416
734
|
exports.init = init;
|
|
417
735
|
exports.isEnabled = isEnabled;
|
|
736
|
+
exports.measureInteraction = measureInteraction;
|
|
737
|
+
exports.printInteractions = printInteractions;
|
|
738
|
+
exports.printOpportunities = printOpportunities;
|
|
418
739
|
exports.printStats = printStats;
|
|
740
|
+
exports.rankOpportunities = rankOpportunities;
|
|
419
741
|
exports.reset = reset;
|
|
420
742
|
exports.subscribe = subscribe;
|
|
743
|
+
exports.summariseInteraction = summarise;
|
|
421
744
|
exports.useRenderDiagnostics = useRenderDiagnostics;
|
|
422
745
|
exports.useTrackedContextValue = useTrackedContextValue;
|
|
423
746
|
exports.useTrackedEffect = useTrackedEffect;
|