react-render-detective 0.5.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 +55 -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-DI5STJM4.js → chunk-3TZNM7JO.js} +66 -3
- 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-OSPTERGK.cjs → chunk-LCGXVRJR.cjs} +66 -3
- 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 +13 -3
- package/dist/core.d.ts +13 -3
- package/dist/core.js +1 -1
- package/dist/index.cjs +72 -274
- 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 +41 -237
- 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 +6 -1
- package/dist/chunk-DI5STJM4.js.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-OSPTERGK.cjs.map +0 -1
- package/dist/chunk-PHYYA67T.cjs.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,96 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { i as ComponentInfo, C as ContextChange, f as Diagnosis, I as Inspected, g as InspectionLimits, M as Mode, P as PropChange, k as PropChangeKind, h as PropValueType, c as RenderPhase, l as RenderTimings, e as Thresholds, T as TrackedStateChange } from './types-
|
|
1
|
+
import { j as Confidence, b as RenderEvent, R as RenderReason, a as DetectiveOptions, D as DetectiveConfig, A as AppStats, d as ComponentStats } from './types-CRET8EhB.js';
|
|
2
|
+
export { i as ComponentInfo, C as ContextChange, f as Diagnosis, I as Inspected, g as InspectionLimits, M as Mode, P as PropChange, k as PropChangeKind, h as PropValueType, c as RenderPhase, l as RenderTimings, S as SelectorChange, e as Thresholds, T as TrackedStateChange } from './types-CRET8EhB.js';
|
|
3
3
|
import { RenderProfile } from './testing.js';
|
|
4
4
|
import { ComponentType, ReactNode, Dispatch, SetStateAction } from 'react';
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* Interaction-scoped attribution.
|
|
8
|
-
*
|
|
9
|
-
* Performance is felt per interaction, not in aggregate — and INP is the metric
|
|
10
|
-
* teams are actually judged on. This joins the two halves: the browser says a
|
|
11
|
-
* keystroke took 240ms, and the render events say which components spent it and
|
|
12
|
-
* why.
|
|
13
|
-
*
|
|
14
|
-
* Uses `PerformanceObserver` with `event` timing, a public browser API. Where it
|
|
15
|
-
* is unsupported (older Safari, jsdom) interaction tracking simply stays empty
|
|
16
|
-
* rather than guessing.
|
|
17
|
-
*/
|
|
18
|
-
interface InteractionRecord {
|
|
19
|
-
id: string;
|
|
20
|
-
/** `click`, `keydown`, `pointerup`… */
|
|
21
|
-
type: string;
|
|
22
|
-
/** Best-effort description of what was interacted with. */
|
|
23
|
-
target?: string;
|
|
24
|
-
startTime: number;
|
|
25
|
-
/** Browser-reported event duration — the number INP is computed from. */
|
|
26
|
-
durationMs: number;
|
|
27
|
-
/**
|
|
28
|
-
* For a manually measured interaction: how long the synchronous action took.
|
|
29
|
-
* Immune to throttling, unlike the full window, which waits on a frame.
|
|
30
|
-
*/
|
|
31
|
-
handlerMs?: number;
|
|
32
|
-
/** Renders committed inside this interaction's window. */
|
|
33
|
-
renders: RenderEvent[];
|
|
34
|
-
/** Sum of self durations for those renders. */
|
|
35
|
-
renderTimeMs: number;
|
|
36
|
-
/** Render time that no observable input change explains. */
|
|
37
|
-
avoidableRenderTimeMs: number;
|
|
38
|
-
}
|
|
39
|
-
interface InteractionSummary {
|
|
40
|
-
interaction: InteractionRecord;
|
|
41
|
-
/** Components ordered by cost within this interaction. */
|
|
42
|
-
contributors: Array<{
|
|
43
|
-
component: string;
|
|
44
|
-
source?: string;
|
|
45
|
-
renders: number;
|
|
46
|
-
totalMs: number;
|
|
47
|
-
cause: string;
|
|
48
|
-
}>;
|
|
49
|
-
headline: string;
|
|
50
|
-
nextStep: string;
|
|
51
|
-
confidence: Confidence;
|
|
52
|
-
}
|
|
53
|
-
interface RawEventTiming {
|
|
54
|
-
name: string;
|
|
55
|
-
startTime: number;
|
|
56
|
-
duration: number;
|
|
57
|
-
target?: string;
|
|
58
|
-
handlerMs?: number;
|
|
59
|
-
}
|
|
60
|
-
declare class InteractionTracker {
|
|
61
|
-
private capacity;
|
|
62
|
-
private records;
|
|
63
|
-
private observer;
|
|
64
|
-
private nextId;
|
|
65
|
-
constructor(capacity?: number);
|
|
66
|
-
/** Returns false when the browser cannot report event timing. */
|
|
67
|
-
start(): boolean;
|
|
68
|
-
stop(): void;
|
|
69
|
-
/** Is the automatic path available in this browser? */
|
|
70
|
-
get automatic(): boolean;
|
|
71
|
-
/**
|
|
72
|
-
* Time an interaction by hand.
|
|
73
|
-
*
|
|
74
|
-
* The automatic path depends on the Event Timing API, which Safari only
|
|
75
|
-
* gained in 16.4 and which does not fire for synthetic input at all — so
|
|
76
|
-
* anything driven by a test harness records nothing. This measures a specific
|
|
77
|
-
* action instead, up to the paint that follows it, and needs no browser
|
|
78
|
-
* support beyond `performance.now`.
|
|
79
|
-
*/
|
|
80
|
-
measure<T>(label: string, action: () => T): T;
|
|
81
|
-
/** Exposed for tests and for `measure`. */
|
|
82
|
-
record(timing: RawEventTiming): InteractionRecord;
|
|
83
|
-
clear(): void;
|
|
84
|
-
/**
|
|
85
|
-
* Joins render events to interactions by commit time. A render belongs to an
|
|
86
|
-
* interaction when it committed between the event starting and shortly after
|
|
87
|
-
* it finished — React commits just after the event handler returns.
|
|
88
|
-
*/
|
|
89
|
-
attribute(events: RenderEvent[]): InteractionRecord[];
|
|
90
|
-
}
|
|
91
|
-
declare function summarise(record: InteractionRecord): InteractionSummary;
|
|
92
|
-
declare function formatInteraction(summary: InteractionSummary): string;
|
|
93
|
-
|
|
94
6
|
/**
|
|
95
7
|
* "Where should I spend my next hour?"
|
|
96
8
|
*
|
|
@@ -171,6 +83,31 @@ interface RenderDetectiveProps {
|
|
|
171
83
|
*/
|
|
172
84
|
declare function RenderDetective({ name, children }: RenderDetectiveProps): ReactNode;
|
|
173
85
|
|
|
86
|
+
interface TrackSelectorOptions {
|
|
87
|
+
/** Readable label — a property path where one could be derived, else the call site. */
|
|
88
|
+
name?: string;
|
|
89
|
+
/** `File.tsx:12:3` of the call, supplied by the build plugin. */
|
|
90
|
+
source?: string;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Records a store value and returns it untouched.
|
|
94
|
+
*
|
|
95
|
+
* Pass-through by construction: the value is returned as given, and nothing
|
|
96
|
+
* here can change what the component receives.
|
|
97
|
+
*/
|
|
98
|
+
declare function trackSelector<T>(value: T, options?: TrackSelectorOptions): T;
|
|
99
|
+
/**
|
|
100
|
+
* Wraps a store hook so its results are attributed. Signature-preserving: extra
|
|
101
|
+
* arguments (react-redux's equality function, for instance) pass straight
|
|
102
|
+
* through, because changing them would change the app's behaviour.
|
|
103
|
+
*
|
|
104
|
+
* const useSelector = createTrackedSelectorHook(useReduxSelector);
|
|
105
|
+
*
|
|
106
|
+
* The build plugin rewrites `useSelector` imports for you; use this directly for
|
|
107
|
+
* a custom store hook, or where the plugin cannot run.
|
|
108
|
+
*/
|
|
109
|
+
declare function createTrackedSelectorHook<A extends unknown[], R>(hook: (...args: A) => R, options?: TrackSelectorOptions): (...args: A) => R;
|
|
110
|
+
|
|
174
111
|
/**
|
|
175
112
|
* In-component diagnostics without wrapping.
|
|
176
113
|
*
|
|
@@ -231,6 +168,13 @@ interface Explanation {
|
|
|
231
168
|
share: number;
|
|
232
169
|
valueType: string;
|
|
233
170
|
}>;
|
|
171
|
+
/** Store selectors that returned a new reference with identical contents. */
|
|
172
|
+
unstableSelectors: Array<{
|
|
173
|
+
name: string;
|
|
174
|
+
source?: string;
|
|
175
|
+
count: number;
|
|
176
|
+
share: number;
|
|
177
|
+
}>;
|
|
234
178
|
averageSelfDuration: number;
|
|
235
179
|
totalSelfDuration: number;
|
|
236
180
|
potentiallyAvoidableRenders: number;
|
|
@@ -280,25 +224,6 @@ declare function explain(componentName: string): string | undefined;
|
|
|
280
224
|
declare function explainStructured(componentName: string): Explanation | undefined;
|
|
281
225
|
/** Snapshot render behaviour for regression testing. See `react-render-detective/testing`. */
|
|
282
226
|
declare function getRenderProfile(scenario: string): RenderProfile;
|
|
283
|
-
/**
|
|
284
|
-
* Interactions, slowest first, with the renders that happened inside each.
|
|
285
|
-
*
|
|
286
|
-
* This is the bridge from render causality to what a user actually feels: the
|
|
287
|
-
* browser reports how long the interaction took, and the render events say
|
|
288
|
-
* which components spent that time and why.
|
|
289
|
-
*/
|
|
290
|
-
declare function getInteractions(): InteractionRecord[];
|
|
291
|
-
/** Structured analysis of one interaction. Defaults to the slowest recorded. */
|
|
292
|
-
declare function explainInteractionStructured(id?: string): InteractionSummary | undefined;
|
|
293
|
-
declare function explainInteraction(id?: string): string | undefined;
|
|
294
|
-
declare function printInteractions(limit?: number): void;
|
|
295
|
-
/**
|
|
296
|
-
* Time one interaction explicitly, up to the paint that follows it.
|
|
297
|
-
*
|
|
298
|
-
* Needed wherever the automatic path cannot see: Safari before 16.4, and any
|
|
299
|
-
* synthetic input, which never produces Event Timing entries.
|
|
300
|
-
*/
|
|
301
|
-
declare function measureInteraction<T>(label: string, action: () => T): T;
|
|
302
227
|
/**
|
|
303
228
|
* Components ranked by estimated recoverable time — the triage view. Render
|
|
304
229
|
* counts answer the wrong question; this answers "what should I fix first?".
|
|
@@ -323,13 +248,8 @@ declare const ReactRenderDetective: {
|
|
|
323
248
|
explainStructured: typeof explainStructured;
|
|
324
249
|
getOpportunities: typeof getOpportunities;
|
|
325
250
|
printOpportunities: typeof printOpportunities;
|
|
326
|
-
getInteractions: typeof getInteractions;
|
|
327
|
-
explainInteraction: typeof explainInteraction;
|
|
328
|
-
explainInteractionStructured: typeof explainInteractionStructured;
|
|
329
|
-
printInteractions: typeof printInteractions;
|
|
330
|
-
measureInteraction: typeof measureInteraction;
|
|
331
251
|
getRenderProfile: typeof getRenderProfile;
|
|
332
252
|
printStats: typeof printStats;
|
|
333
253
|
};
|
|
334
254
|
|
|
335
|
-
export { AppStats, ComponentStats, Confidence, DetectiveConfig, DetectiveOptions, type Explanation, type
|
|
255
|
+
export { AppStats, ComponentStats, Confidence, DetectiveConfig, DetectiveOptions, type Explanation, type Opportunity, ReactRenderDetective, RenderDetective, type RenderDetectiveProps, RenderEvent, RenderReason, type TrackOptions, type TrackSelectorOptions, clear, configure, createTrackedSelectorHook, explain, explainEvents, explainStructured, formatExplanation, formatOpportunities, getComponentStats, getConfig, getEvents, getOpportunities, getRenderProfile, getStats, init, isEnabled, printOpportunities, printStats, rankOpportunities, reset, subscribe, trackSelector, useRenderDiagnostics, useTrackedContextValue, useTrackedEffect, useTrackedState, withRenderDetective };
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { explainEvents, formatExplanation } from './chunk-
|
|
2
|
-
export { explainEvents, formatExplanation } from './chunk-
|
|
3
|
-
import { getDetective, inspect, shallowEqual, diffProps, formatInspected } from './chunk-DI5STJM4.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
9
|
// src/console/style.ts
|
|
@@ -217,6 +217,8 @@ function label(event) {
|
|
|
217
217
|
return "context update";
|
|
218
218
|
case "state":
|
|
219
219
|
return "state changed";
|
|
220
|
+
case "store":
|
|
221
|
+
return "store update";
|
|
220
222
|
case "state-or-external":
|
|
221
223
|
return "state or external store";
|
|
222
224
|
case "unknown":
|
|
@@ -224,194 +226,6 @@ function label(event) {
|
|
|
224
226
|
}
|
|
225
227
|
}
|
|
226
228
|
|
|
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
229
|
// src/core/opportunities.ts
|
|
416
230
|
var DEFAULT_MIN_SAVING_MS = 1;
|
|
417
231
|
function rankOpportunities({ events, lifecycles, minSavingMs = DEFAULT_MIN_SAVING_MS }) {
|
|
@@ -530,6 +344,38 @@ function RenderDetective({ name, children }) {
|
|
|
530
344
|
const { node, onRender } = useInstrumentedNode(name, { children });
|
|
531
345
|
return renderInstrumented(node, onRender, children);
|
|
532
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
|
+
}
|
|
533
379
|
function useRenderDiagnostics(name, props) {
|
|
534
380
|
const detective = getDetective();
|
|
535
381
|
const parent = useContext(AncestryContext);
|
|
@@ -564,11 +410,11 @@ function useRenderDiagnostics(name, props) {
|
|
|
564
410
|
}
|
|
565
411
|
return last.current;
|
|
566
412
|
}
|
|
567
|
-
var
|
|
413
|
+
var useOwnerId2 = typeof React.useId === "function" ? React.useId : () => "";
|
|
568
414
|
function useTrackedState(name, initial) {
|
|
569
415
|
const detective = getDetective();
|
|
570
416
|
const node = useContext(AncestryContext);
|
|
571
|
-
const ownerId =
|
|
417
|
+
const ownerId = useOwnerId2();
|
|
572
418
|
const [state, setState] = useState(initial);
|
|
573
419
|
const previous = useRef(state);
|
|
574
420
|
if (node && node.stateOwner === void 0) node.stateOwner = ownerId;
|
|
@@ -629,21 +475,12 @@ function isRecord(v) {
|
|
|
629
475
|
|
|
630
476
|
// src/index.ts
|
|
631
477
|
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
|
-
}
|
|
638
478
|
function init(options = {}) {
|
|
639
479
|
const detective = getDetective();
|
|
640
480
|
detective.init(options);
|
|
641
481
|
const g = globalThis;
|
|
642
482
|
g[REPORTER]?.();
|
|
643
483
|
g[REPORTER] = void 0;
|
|
644
|
-
if (detective.enabled) {
|
|
645
|
-
tracker().start();
|
|
646
|
-
}
|
|
647
484
|
if (detective.enabled && detective.config.mode !== "silent") {
|
|
648
485
|
g[REPORTER] = attachConsoleReporter(detective);
|
|
649
486
|
} else if (!detective.enabled) {
|
|
@@ -675,14 +512,11 @@ function subscribe(listener) {
|
|
|
675
512
|
}
|
|
676
513
|
function clear() {
|
|
677
514
|
getDetective().clear();
|
|
678
|
-
tracker().clear();
|
|
679
515
|
}
|
|
680
516
|
function reset() {
|
|
681
517
|
const g = globalThis;
|
|
682
518
|
g[REPORTER]?.();
|
|
683
519
|
g[REPORTER] = void 0;
|
|
684
|
-
g[INTERACTIONS]?.stop();
|
|
685
|
-
g[INTERACTIONS] = void 0;
|
|
686
520
|
getDetective().reset();
|
|
687
521
|
}
|
|
688
522
|
function explain(componentName2) {
|
|
@@ -697,31 +531,6 @@ function getRenderProfile(scenario) {
|
|
|
697
531
|
for (const stats of getDetective().getComponentStats()) remounts[stats.name] = stats.remountCount;
|
|
698
532
|
return profileFromEvents(scenario, getEvents(), remounts);
|
|
699
533
|
}
|
|
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
534
|
function getOpportunities(limit = 10) {
|
|
726
535
|
const detective = getDetective();
|
|
727
536
|
const lifecycles = /* @__PURE__ */ new Map();
|
|
@@ -780,15 +589,10 @@ var ReactRenderDetective = {
|
|
|
780
589
|
explainStructured,
|
|
781
590
|
getOpportunities,
|
|
782
591
|
printOpportunities,
|
|
783
|
-
getInteractions,
|
|
784
|
-
explainInteraction,
|
|
785
|
-
explainInteractionStructured,
|
|
786
|
-
printInteractions,
|
|
787
|
-
measureInteraction,
|
|
788
592
|
getRenderProfile,
|
|
789
593
|
printStats
|
|
790
594
|
};
|
|
791
595
|
|
|
792
|
-
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 };
|
|
793
597
|
//# sourceMappingURL=index.js.map
|
|
794
598
|
//# sourceMappingURL=index.js.map
|