react-render-detective 0.3.0 → 0.5.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/dist/index.js CHANGED
@@ -1,10 +1,45 @@
1
- import { formatExplanation, explainEvents } from './chunk-HOTY7J3X.js';
1
+ import { explainEvents, formatExplanation } from './chunk-HOTY7J3X.js';
2
2
  export { explainEvents, formatExplanation } from './chunk-HOTY7J3X.js';
3
- import { getDetective, inspect, shallowEqual, diffProps, formatInspected } from './chunk-QOGTEVBP.js';
3
+ import { getDetective, inspect, shallowEqual, diffProps, formatInspected } from './chunk-DI5STJM4.js';
4
+ import { profileFromEvents } from './chunk-LILK23YH.js';
4
5
  import * as React from 'react';
5
6
  import { createContext, useContext, useState, useRef, useEffect, useLayoutEffect, useCallback, Profiler } from 'react';
6
7
  import { jsx } from 'react/jsx-runtime';
7
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
+
8
43
  // src/console/reporter.ts
9
44
  var ICON = {
10
45
  normal: "\xB7",
@@ -13,30 +48,102 @@ var ICON = {
13
48
  "very-slow": "\u25B2\u25B2",
14
49
  critical: "\u25A0"
15
50
  };
51
+ var BATCH_WINDOW_MS = 400;
16
52
  function attachConsoleReporter(detective) {
17
- return detective.subscribe((event) => {
18
- const mode = detective.config.mode;
19
- if (mode === "silent") return;
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;
20
60
  try {
21
- if (mode === "verbose") printVerbose(event);
22
- else printConcise(event, detective.config.slowRenderThreshold);
61
+ printBatch(batch, detective.config.slowRenderThreshold);
23
62
  } catch {
24
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
+ }
25
80
  });
81
+ return () => {
82
+ unsubscribe();
83
+ if (timer !== void 0) clearTimeout(timer);
84
+ };
26
85
  }
27
- function printConcise(event, slowThreshold) {
28
- const { diagnosis, timings, component } = event;
29
- const changed = event.changedProps.map((c) => c.key).join(", ");
30
- const parts = [
31
- `[RRD] ${component.name} #${event.renderNumber}`,
32
- `Reason: ${label(event)}`,
33
- changed ? `Changed: ${changed}` : void 0,
34
- timings.subtreeDuration > 0 ? `Duration: ${timings.selfDuration.toFixed(1)}ms` : void 0
35
- ].filter(Boolean);
36
- const line = `${ICON[diagnosis.severity] ?? "\xB7"} ${parts.join(" ")}`;
37
- const slow = timings.selfDuration >= slowThreshold;
38
- if (slow) console.warn(line);
39
- else console.log(line);
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));
40
147
  }
41
148
  function printVerbose(event) {
42
149
  const { diagnosis, timings } = event;
@@ -59,9 +166,7 @@ function printVerbose(event) {
59
166
  for (const c of event.changedProps) console.log(propLine(c));
60
167
  console.groupEnd();
61
168
  }
62
- if (event.unchangedProps.length > 0) {
63
- console.log(`Props same: ${event.unchangedProps.join(", ")}`);
64
- }
169
+ if (event.unchangedProps.length > 0) console.log(`Props same: ${event.unchangedProps.join(", ")}`);
65
170
  if (event.trackedState.length > 0) {
66
171
  console.log(
67
172
  `State: ${event.trackedState.map((s) => `${s.name}: ${formatInspected(s.previous)} \u2192 ${formatInspected(s.current)}`).join(", ")}`
@@ -118,6 +223,245 @@ function label(event) {
118
223
  return "undetermined";
119
224
  }
120
225
  }
226
+
227
+ // src/core/interactions.ts
228
+ var COMMIT_SLACK_MS = 100;
229
+ var FALLBACK_CLOSE_MS = 50;
230
+ var InteractionTracker = class {
231
+ constructor(capacity = 50) {
232
+ this.capacity = capacity;
233
+ this.records = [];
234
+ this.nextId = 0;
235
+ }
236
+ /** Returns false when the browser cannot report event timing. */
237
+ start() {
238
+ if (this.observer) return true;
239
+ const PO = globalThis.PerformanceObserver;
240
+ const supported = PO?.supportedEntryTypes?.includes("event");
241
+ if (!PO || !supported) return false;
242
+ try {
243
+ const observer = new PO((list) => {
244
+ for (const entry of list.getEntries()) {
245
+ const timing = entry;
246
+ this.record({
247
+ name: timing.name,
248
+ startTime: timing.startTime,
249
+ duration: timing.duration,
250
+ target: describeTarget(timing.target)
251
+ });
252
+ }
253
+ });
254
+ observer.observe({ type: "event", buffered: true, durationThreshold: 16 });
255
+ this.observer = observer;
256
+ return true;
257
+ } catch {
258
+ return false;
259
+ }
260
+ }
261
+ stop() {
262
+ this.observer?.disconnect();
263
+ this.observer = void 0;
264
+ }
265
+ /** Is the automatic path available in this browser? */
266
+ get automatic() {
267
+ return this.observer !== void 0;
268
+ }
269
+ /**
270
+ * Time an interaction by hand.
271
+ *
272
+ * The automatic path depends on the Event Timing API, which Safari only
273
+ * gained in 16.4 and which does not fire for synthetic input at all — so
274
+ * anything driven by a test harness records nothing. This measures a specific
275
+ * action instead, up to the paint that follows it, and needs no browser
276
+ * support beyond `performance.now`.
277
+ */
278
+ measure(label2, action) {
279
+ const startTime = now();
280
+ let handlerMs = 0;
281
+ let finished = false;
282
+ const finish = () => {
283
+ if (finished) return;
284
+ finished = true;
285
+ this.record({ name: label2, startTime, duration: now() - startTime, handlerMs });
286
+ };
287
+ let result;
288
+ try {
289
+ result = action();
290
+ handlerMs = now() - startTime;
291
+ } catch (error) {
292
+ handlerMs = now() - startTime;
293
+ finish();
294
+ throw error;
295
+ }
296
+ const raf = globalThis.requestAnimationFrame;
297
+ if (raf) raf(() => raf(finish));
298
+ setTimeout(finish, FALLBACK_CLOSE_MS);
299
+ return result;
300
+ }
301
+ /** Exposed for tests and for `measure`. */
302
+ record(timing) {
303
+ const record = {
304
+ id: `interaction_${++this.nextId}`,
305
+ type: timing.name,
306
+ target: timing.target,
307
+ startTime: timing.startTime,
308
+ durationMs: timing.duration,
309
+ handlerMs: timing.handlerMs,
310
+ renders: [],
311
+ renderTimeMs: 0,
312
+ avoidableRenderTimeMs: 0
313
+ };
314
+ this.records.push(record);
315
+ if (this.records.length > this.capacity) this.records.shift();
316
+ return record;
317
+ }
318
+ clear() {
319
+ this.records.length = 0;
320
+ }
321
+ /**
322
+ * Joins render events to interactions by commit time. A render belongs to an
323
+ * interaction when it committed between the event starting and shortly after
324
+ * it finished — React commits just after the event handler returns.
325
+ */
326
+ attribute(events) {
327
+ for (const record of this.records) {
328
+ const from = record.startTime;
329
+ const to = record.startTime + record.durationMs + COMMIT_SLACK_MS;
330
+ record.renders = events.filter((e) => e.timings.commitTime >= from && e.timings.commitTime <= to);
331
+ record.renderTimeMs = record.renders.reduce((a, e) => a + e.timings.selfDuration, 0);
332
+ record.avoidableRenderTimeMs = record.renders.filter((e) => e.diagnosis.potentiallyAvoidable).reduce((a, e) => a + e.timings.selfDuration, 0);
333
+ }
334
+ return [...this.records].sort((a, b) => b.durationMs - a.durationMs);
335
+ }
336
+ };
337
+ function summarise(record) {
338
+ const byComponent = /* @__PURE__ */ new Map();
339
+ for (const event of record.renders) {
340
+ const entry = byComponent.get(event.component.name) ?? {
341
+ renders: 0,
342
+ totalMs: 0,
343
+ source: event.component.source,
344
+ cause: event.diagnosis.reason
345
+ };
346
+ entry.renders++;
347
+ entry.totalMs += event.timings.selfDuration;
348
+ byComponent.set(event.component.name, entry);
349
+ }
350
+ 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);
351
+ const top = contributors[0];
352
+ const accounted = (record.handlerMs ?? record.durationMs) + record.renderTimeMs;
353
+ const idleWindow = record.handlerMs !== void 0 && record.durationMs > accounted * 3 && record.durationMs - accounted > 100;
354
+ const effectiveMs = idleWindow ? accounted : record.durationMs;
355
+ const share = effectiveMs > 0 ? record.renderTimeMs / effectiveMs : 0;
356
+ let headline;
357
+ let nextStep;
358
+ let confidence = "medium";
359
+ if (idleWindow) {
360
+ 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.`;
361
+ 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.";
362
+ return { interaction: record, contributors, headline, nextStep, confidence: "medium" };
363
+ }
364
+ if (record.renders.length === 0) {
365
+ headline = `${record.type} took ${fmt(record.durationMs)}, and no instrumented component rendered inside it.`;
366
+ 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.";
367
+ confidence = "low";
368
+ } else if (share >= 0.4 && record.avoidableRenderTimeMs > 0) {
369
+ 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.`;
370
+ 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.";
371
+ confidence = "high";
372
+ } else if (share >= 0.4) {
373
+ headline = `${record.type} took ${fmt(record.durationMs)}; ${fmt(record.renderTimeMs)} of it was rendering, all of it explained by real input changes.`;
374
+ nextStep = "This is genuine work. Make the renders cheaper rather than fewer \u2014 or do less of it per interaction.";
375
+ confidence = "high";
376
+ } else {
377
+ headline = `${record.type} took ${fmt(record.durationMs)}, but only ${fmt(record.renderTimeMs)} was React rendering.`;
378
+ nextStep = "Most of the cost is outside rendering \u2014 event handlers, layout or paint. A browser profile will show more than this tool can.";
379
+ confidence = "medium";
380
+ }
381
+ return { interaction: record, contributors, headline, nextStep, confidence };
382
+ }
383
+ function formatInteraction(summary) {
384
+ const { interaction: i } = summary;
385
+ const lines = [
386
+ `${i.type}${i.target ? ` on ${i.target}` : ""} ${fmt(i.durationMs)}`,
387
+ "",
388
+ summary.headline
389
+ ];
390
+ if (summary.contributors.length > 0) {
391
+ lines.push("", "Rendering inside this interaction");
392
+ for (const c of summary.contributors.slice(0, 8)) {
393
+ lines.push(
394
+ ` ${c.component.padEnd(22)} ${String(c.renders).padStart(4)} render(s) ${fmt(c.totalMs).padStart(8)} ${c.cause}`
395
+ );
396
+ }
397
+ }
398
+ lines.push("", `Next step`, ` ${summary.nextStep}`, "", `Confidence: ${summary.confidence}`);
399
+ return lines.join("\n");
400
+ }
401
+ function describeTarget(target) {
402
+ if (!target || typeof target !== "object") return void 0;
403
+ const el = target;
404
+ if (!el.tagName) return void 0;
405
+ const tag = el.tagName.toLowerCase();
406
+ if (el.id) return `${tag}#${el.id}`;
407
+ const className = typeof el.className === "string" ? el.className.trim().split(/\s+/)[0] : void 0;
408
+ if (className) return `${tag}.${className}`;
409
+ const text = el.textContent?.trim().slice(0, 20);
410
+ return text ? `${tag} "${text}"` : tag;
411
+ }
412
+ var fmt = (ms) => `${ms.toFixed(1)}ms`;
413
+ var now = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
414
+
415
+ // src/core/opportunities.ts
416
+ var DEFAULT_MIN_SAVING_MS = 1;
417
+ function rankOpportunities({ events, lifecycles, minSavingMs = DEFAULT_MIN_SAVING_MS }) {
418
+ const names = new Set(events.map((e) => e.component.name));
419
+ const out = [];
420
+ for (const name of names) {
421
+ const lifecycle = lifecycles.get(name);
422
+ const explanation = explainEvents(name, events, lifecycle);
423
+ if (!explanation) continue;
424
+ const mounts = events.filter((e) => e.component.name === name && e.phase === "mount");
425
+ const averageMountCost = mounts.length ? mounts.reduce((a, e) => a + e.timings.selfDuration, 0) / mounts.length : 0;
426
+ const remountSaving = explanation.remounts * averageMountCost;
427
+ const estimatedSavingMs = explanation.estimatedAvoidableTime + remountSaving;
428
+ if (estimatedSavingMs < minSavingMs) continue;
429
+ out.push({
430
+ component: name,
431
+ source: explanation.source,
432
+ estimatedSavingMs,
433
+ avoidableRenders: explanation.potentiallyAvoidableRenders,
434
+ remounts: explanation.remounts,
435
+ averageSelfDuration: explanation.averageSelfDuration,
436
+ summary: explanation.headline,
437
+ nextStep: explanation.nextStep,
438
+ confidence: explanation.confidence
439
+ });
440
+ }
441
+ return out.sort((a, b) => b.estimatedSavingMs - a.estimatedSavingMs);
442
+ }
443
+ function formatOpportunities(opportunities) {
444
+ if (opportunities.length === 0) {
445
+ return "React Render Detective\n\nNo measurable render waste found yet. Interact with the app and try again.";
446
+ }
447
+ const lines = [
448
+ "React Render Detective \u2014 where to spend your next hour",
449
+ "",
450
+ "Ranked by estimated recoverable time. These are estimates, not promises:",
451
+ "measure each fix.",
452
+ ""
453
+ ];
454
+ for (const [index, o] of opportunities.entries()) {
455
+ lines.push(
456
+ `${String(index + 1).padStart(2)}. ${o.component}${o.source ? ` ${o.source}` : ""}`,
457
+ ` ~${o.estimatedSavingMs.toFixed(0)}ms recoverable ${o.avoidableRenders} avoidable render${o.avoidableRenders === 1 ? "" : "s"}${o.remounts > 0 ? `, ${o.remounts}\xD7 rebuilt` : ""} (confidence: ${o.confidence})`,
458
+ ` ${o.summary}`,
459
+ ` \u2192 ${o.nextStep}`,
460
+ ""
461
+ );
462
+ }
463
+ return lines.join("\n");
464
+ }
121
465
  var AncestryContext = createContext(void 0);
122
466
  AncestryContext.displayName = "RenderDetectiveAncestry";
123
467
  function useInstrumentedNode(name, props, source) {
@@ -285,12 +629,21 @@ function isRecord(v) {
285
629
 
286
630
  // src/index.ts
287
631
  var REPORTER = /* @__PURE__ */ Symbol.for("react-render-detective.reporter");
632
+ var INTERACTIONS = /* @__PURE__ */ Symbol.for("react-render-detective.interactions");
633
+ function tracker() {
634
+ const g = globalThis;
635
+ if (!g[INTERACTIONS]) g[INTERACTIONS] = new InteractionTracker();
636
+ return g[INTERACTIONS];
637
+ }
288
638
  function init(options = {}) {
289
639
  const detective = getDetective();
290
640
  detective.init(options);
291
641
  const g = globalThis;
292
642
  g[REPORTER]?.();
293
643
  g[REPORTER] = void 0;
644
+ if (detective.enabled) {
645
+ tracker().start();
646
+ }
294
647
  if (detective.enabled && detective.config.mode !== "silent") {
295
648
  g[REPORTER] = attachConsoleReporter(detective);
296
649
  } else if (!detective.enabled) {
@@ -322,11 +675,14 @@ function subscribe(listener) {
322
675
  }
323
676
  function clear() {
324
677
  getDetective().clear();
678
+ tracker().clear();
325
679
  }
326
680
  function reset() {
327
681
  const g = globalThis;
328
682
  g[REPORTER]?.();
329
683
  g[REPORTER] = void 0;
684
+ g[INTERACTIONS]?.stop();
685
+ g[INTERACTIONS] = void 0;
330
686
  getDetective().reset();
331
687
  }
332
688
  function explain(componentName2) {
@@ -336,6 +692,47 @@ function explain(componentName2) {
336
692
  function explainStructured(componentName2) {
337
693
  return explainEvents(componentName2, getEvents(), getDetective().lifecycleOf(componentName2));
338
694
  }
695
+ function getRenderProfile(scenario) {
696
+ const remounts = {};
697
+ for (const stats of getDetective().getComponentStats()) remounts[stats.name] = stats.remountCount;
698
+ return profileFromEvents(scenario, getEvents(), remounts);
699
+ }
700
+ function getInteractions() {
701
+ return tracker().attribute(getEvents());
702
+ }
703
+ function explainInteractionStructured(id) {
704
+ const records = getInteractions();
705
+ const record = id ? records.find((r) => r.id === id) : records[0];
706
+ return record ? summarise(record) : void 0;
707
+ }
708
+ function explainInteraction(id) {
709
+ const summary = explainInteractionStructured(id);
710
+ return summary ? formatInteraction(summary) : void 0;
711
+ }
712
+ function printInteractions(limit = 5) {
713
+ const records = getInteractions().slice(0, limit);
714
+ if (records.length === 0) {
715
+ console.log(
716
+ 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."
717
+ );
718
+ return;
719
+ }
720
+ console.log(records.map((r) => formatInteraction(summarise(r))).join("\n\n"));
721
+ }
722
+ function measureInteraction(label2, action) {
723
+ return tracker().measure(label2, action);
724
+ }
725
+ function getOpportunities(limit = 10) {
726
+ const detective = getDetective();
727
+ const lifecycles = /* @__PURE__ */ new Map();
728
+ for (const stats of detective.getComponentStats()) {
729
+ lifecycles.set(stats.name, { remounts: stats.remountCount });
730
+ }
731
+ return rankOpportunities({ events: detective.getEvents(), lifecycles }).slice(0, limit);
732
+ }
733
+ function printOpportunities(limit = 10) {
734
+ console.log(formatOpportunities(getOpportunities(limit)));
735
+ }
339
736
  function printStats() {
340
737
  const s = getStats();
341
738
  const lines = [
@@ -381,9 +778,17 @@ var ReactRenderDetective = {
381
778
  reset,
382
779
  explain,
383
780
  explainStructured,
781
+ getOpportunities,
782
+ printOpportunities,
783
+ getInteractions,
784
+ explainInteraction,
785
+ explainInteractionStructured,
786
+ printInteractions,
787
+ measureInteraction,
788
+ getRenderProfile,
384
789
  printStats
385
790
  };
386
791
 
387
- export { ReactRenderDetective, RenderDetective, clear, configure, explain, explainStructured, getComponentStats, getConfig, getEvents, getStats, init, isEnabled, printStats, reset, subscribe, useRenderDiagnostics, useTrackedContextValue, useTrackedEffect, useTrackedState, withRenderDetective };
792
+ export { InteractionTracker, ReactRenderDetective, RenderDetective, clear, configure, explain, explainInteraction, explainInteractionStructured, explainStructured, formatInteraction, formatOpportunities, getComponentStats, getConfig, getEvents, getInteractions, getOpportunities, getRenderProfile, getStats, init, isEnabled, measureInteraction, printInteractions, printOpportunities, printStats, rankOpportunities, reset, subscribe, summarise as summariseInteraction, useRenderDiagnostics, useTrackedContextValue, useTrackedEffect, useTrackedState, withRenderDetective };
388
793
  //# sourceMappingURL=index.js.map
389
794
  //# sourceMappingURL=index.js.map