rerender-lens 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.
Files changed (66) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README.md +192 -353
  3. package/dist/{budget-COBu7jBU.d.cts → budget-BnJlYwrV.d.ts} +20 -10
  4. package/dist/{budget-LkNjGtRc.d.ts → budget-CRswn-nC.d.cts} +20 -10
  5. package/dist/cli.cjs +248 -31
  6. package/dist/cli.cjs.map +1 -1
  7. package/dist/cli.js +248 -31
  8. package/dist/cli.js.map +1 -1
  9. package/dist/{devtools-BHGUUo-p.d.ts → devtools-BYWat7nQ.d.ts} +17 -3
  10. package/dist/{devtools-DmhWiWcN.d.cts → devtools-DJi_6AfJ.d.cts} +17 -3
  11. package/dist/index.cjs +635 -109
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.d.cts +131 -8
  14. package/dist/index.d.ts +131 -8
  15. package/dist/index.js +620 -110
  16. package/dist/index.js.map +1 -1
  17. package/dist/jest-setup.cjs +1673 -0
  18. package/dist/jest-setup.cjs.map +1 -0
  19. package/dist/jest-setup.d.cts +8 -0
  20. package/dist/jest-setup.d.ts +8 -0
  21. package/dist/jest-setup.js +1671 -0
  22. package/dist/jest-setup.js.map +1 -0
  23. package/dist/jest.cjs +1760 -0
  24. package/dist/jest.cjs.map +1 -0
  25. package/dist/jest.d.cts +23 -0
  26. package/dist/jest.d.ts +23 -0
  27. package/dist/jest.js +1751 -0
  28. package/dist/jest.js.map +1 -0
  29. package/dist/{notifiers-BjjGSHpp.d.ts → notifiers-DwKlvv4H.d.cts} +8 -4
  30. package/dist/{notifiers-BGQtWKfX.d.cts → notifiers-FNzKHs0a.d.ts} +8 -4
  31. package/dist/playwright.cjs +235 -30
  32. package/dist/playwright.cjs.map +1 -1
  33. package/dist/playwright.d.cts +8 -6
  34. package/dist/playwright.d.ts +8 -6
  35. package/dist/playwright.js +235 -31
  36. package/dist/playwright.js.map +1 -1
  37. package/dist/relay.cjs +10 -2
  38. package/dist/relay.cjs.map +1 -1
  39. package/dist/relay.js +10 -2
  40. package/dist/relay.js.map +1 -1
  41. package/dist/rerender-lens.iife.js +1100 -337
  42. package/dist/runner-report-B8Sc3FZO.d.cts +46 -0
  43. package/dist/runner-report-CW8X5RwK.d.ts +46 -0
  44. package/dist/setup.cjs +308 -95
  45. package/dist/setup.cjs.map +1 -1
  46. package/dist/setup.js +308 -95
  47. package/dist/setup.js.map +1 -1
  48. package/dist/{types-BzUEVkxJ.d.cts → types-DW5-N2XH.d.cts} +55 -2
  49. package/dist/{types-BzUEVkxJ.d.ts → types-DW5-N2XH.d.ts} +55 -2
  50. package/dist/vite.d.cts +1 -1
  51. package/dist/vite.d.ts +1 -1
  52. package/dist/vitest-setup.cjs +452 -87
  53. package/dist/vitest-setup.cjs.map +1 -1
  54. package/dist/vitest-setup.d.cts +4 -4
  55. package/dist/vitest-setup.d.ts +4 -4
  56. package/dist/vitest-setup.js +452 -87
  57. package/dist/vitest-setup.js.map +1 -1
  58. package/dist/vitest.cjs +515 -123
  59. package/dist/vitest.cjs.map +1 -1
  60. package/dist/vitest.d.cts +21 -39
  61. package/dist/vitest.d.ts +21 -39
  62. package/dist/vitest.js +516 -124
  63. package/dist/vitest.js.map +1 -1
  64. package/package.json +41 -5
  65. package/panel/panel.css +1 -34
  66. package/panel/panel.js +865 -713
package/dist/index.cjs CHANGED
@@ -28,10 +28,49 @@ function remember(seen, a, b) {
28
28
  set.add(b);
29
29
  return false;
30
30
  }
31
+ var scope = null;
32
+ var DEFAULT_DIFF_BUDGET = 5e4;
33
+ function beginDiffScope(budget = 2e5) {
34
+ const previous = scope;
35
+ scope = { cache: /* @__PURE__ */ new WeakMap(), remaining: budget, exhausted: false };
36
+ return () => {
37
+ scope = previous;
38
+ };
39
+ }
40
+ function diffBudgetExhausted() {
41
+ return scope?.exhausted === true;
42
+ }
31
43
  function deepEqual(a, b, seen = /* @__PURE__ */ new Map()) {
32
44
  if (Object.is(a, b)) return true;
33
45
  if (typeof a !== typeof b) return false;
34
46
  if (typeof a !== "object" || a === null || b === null) return false;
47
+ if (!scope) {
48
+ const end = beginDiffScope(DEFAULT_DIFF_BUDGET);
49
+ try {
50
+ return deepEqual(a, b, seen);
51
+ } finally {
52
+ end();
53
+ }
54
+ }
55
+ const sc = scope;
56
+ const objA = a;
57
+ const objB = b;
58
+ const hit = sc.cache.get(objA)?.get(objB);
59
+ if (hit !== void 0) return hit;
60
+ if (--sc.remaining < 0) {
61
+ sc.exhausted = true;
62
+ return false;
63
+ }
64
+ const result = deepEqualObjects(objA, objB, seen);
65
+ let m = sc.cache.get(objA);
66
+ if (!m) {
67
+ m = /* @__PURE__ */ new WeakMap();
68
+ sc.cache.set(objA, m);
69
+ }
70
+ m.set(objB, result);
71
+ return result;
72
+ }
73
+ function deepEqualObjects(a, b, seen) {
35
74
  const objA = a;
36
75
  const objB = b;
37
76
  if (remember(seen, objA, objB)) return true;
@@ -60,6 +99,10 @@ function deepEqual(a, b, seen = /* @__PURE__ */ new Map()) {
60
99
  }
61
100
  if (a instanceof Set) {
62
101
  if (!(b instanceof Set) || a.size !== b.size) return false;
102
+ if (a.size > 50) {
103
+ for (const v of a) if (!b.has(v)) return false;
104
+ return true;
105
+ }
63
106
  outer: for (const v of a) {
64
107
  if (b.has(v)) continue;
65
108
  for (const w of b) if (deepEqual(v, w, seen)) continue outer;
@@ -75,14 +118,16 @@ function deepEqual(a, b, seen = /* @__PURE__ */ new Map()) {
75
118
  const ka = Object.keys(a);
76
119
  const kb = Object.keys(b);
77
120
  if (ka.length !== kb.length) return false;
121
+ const rb = b;
78
122
  for (const k of ka) {
79
123
  if (!Object.prototype.hasOwnProperty.call(b, k)) return false;
80
- if (!deepEqual(a[k], b[k], seen)) return false;
124
+ if (!deepEqual(a[k], rb[k], seen)) return false;
81
125
  }
82
126
  return true;
83
127
  }
84
128
  return false;
85
129
  }
130
+ var AVOIDABLE_KINDS = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
86
131
  function classify(prev, next) {
87
132
  if (typeof prev === "function" && typeof next === "function") {
88
133
  return prev.name === next.name && prev.toString() === next.toString() ? "function" : "different";
@@ -119,30 +164,47 @@ function diffRecords(prev, next, basePath = "") {
119
164
  if (Object.is(a, b)) continue;
120
165
  const kind = classify(a, b);
121
166
  if (kind === "different") {
122
- changes.push({ path: firstDifferentPath(a, b, path), kind, prev: a, next: b });
167
+ changes.push({ path: diffBudgetExhausted() ? path : firstDifferentPath(a, b, path) ?? path, kind, prev: a, next: b });
123
168
  } else {
124
169
  changes.push({ path, kind, prev: a, next: b });
125
170
  }
126
171
  }
127
172
  return changes;
128
173
  }
129
- function firstDifferentPath(a, b, path, depth = 0) {
130
- if (depth > 8) return path;
174
+ function firstDifferentPath(a, b, path = "", depth = 0) {
175
+ if (depth > 8) return path || "(value)";
131
176
  if (Array.isArray(a) && Array.isArray(b)) {
132
- if (a.length !== b.length) return `${path}.length`;
177
+ if (a.length !== b.length) return path ? `${path}.length` : "length";
133
178
  for (let i = 0; i < a.length; i++) {
134
179
  if (!deepEqual(a[i], b[i])) return firstDifferentPath(a[i], b[i], joinPath(path, i), depth + 1);
135
180
  }
136
- return path;
181
+ return null;
137
182
  }
138
183
  if (isPlainObject(a) && isPlainObject(b)) {
139
184
  const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
140
185
  for (const k of keys) {
141
186
  if (!deepEqual(a[k], b[k])) return firstDifferentPath(a[k], b[k], joinPath(path, k), depth + 1);
142
187
  }
143
- return path;
188
+ return null;
144
189
  }
145
- return path;
190
+ return deepEqual(a, b) ? null : path || "(value)";
191
+ }
192
+ function diffLeaves(a, b, limit = 20, path = "", out = []) {
193
+ if (out.length >= limit || Object.is(a, b)) return out;
194
+ if (Array.isArray(a) && Array.isArray(b)) {
195
+ const n = Math.max(a.length, b.length);
196
+ for (let i = 0; i < n && out.length < limit; i++) diffLeaves(a[i], b[i], limit, joinPath(path, i), out);
197
+ return out;
198
+ }
199
+ if (isPlainObject(a) && isPlainObject(b)) {
200
+ for (const k of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) {
201
+ if (out.length >= limit) break;
202
+ diffLeaves(a[k], b[k], limit, joinPath(path, k), out);
203
+ }
204
+ return out;
205
+ }
206
+ if (!deepEqual(a, b)) out.push({ path: path || "(value)", prev: a, next: b });
207
+ return out;
146
208
  }
147
209
 
148
210
  // src/report.ts
@@ -160,10 +222,12 @@ function storeAdvice(custom) {
160
222
  }
161
223
  var isGenuine = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
162
224
  var isChildren = (change) => change.path === "children";
225
+ var isRefObject = (change) => change.path === "ref" && change.kind === "deep-equal";
163
226
  function fixFor(change) {
164
227
  if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
165
228
  return `lift the children out of the parent's render: memoize them with useMemo, hoist static elements to module scope, or render them from a component that does not re-render`;
166
229
  }
230
+ if (isRefObject(change)) return `create the ref once with useRef (or createRef outside the render) and pass the same object every time`;
167
231
  switch (change.kind) {
168
232
  case "function":
169
233
  return `wrap it in useCallback (or hoist it out of the parent's render)`;
@@ -179,6 +243,7 @@ function describe(change) {
179
243
  if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
180
244
  return `children are new React elements with the same types and props on every render of the parent`;
181
245
  }
246
+ if (isRefObject(change)) return `prop "ref" is a new ref object on every render (React re-attaches it each time)`;
182
247
  switch (change.kind) {
183
248
  case "deep-equal":
184
249
  return `prop "${change.path}" is a new reference but deep-equal to the previous value`;
@@ -206,11 +271,21 @@ function buildReport(input) {
206
271
  if (causes.length === 0) trigger = "parent";
207
272
  else if (causes.length === 1) trigger = causes[0];
208
273
  else trigger = "mixed";
209
- const avoidable = trigger === "parent";
274
+ const nothingChanged = input.propChanges.length === 0 && stateChanges.length === 0 && hookChanges.length === 0;
275
+ const cacheHit = input.compiled === true && trigger === "parent" && input.propChanges.length === 0;
276
+ const revealed = input.revealed === true && trigger === "parent";
277
+ const avoidable = trigger === "parent" && !cacheHit && !revealed;
210
278
  const reasons = [];
211
- if (avoidable && input.propChanges.length === 0 && stateChanges.length === 0 && hookChanges.length === 0) {
279
+ const memoFix = input.classComponent ? `Make "${input.component}" extend PureComponent (or implement shouldComponentUpdate)` : `Wrap "${input.component}" in React.memo (or extend PureComponent)`;
280
+ if (cacheHit) {
281
+ reasons.push(`compiled by React Compiler: its output is memoized and the inputs did not change, so this render is cheap; nothing to fix.`);
282
+ } else if (revealed) {
283
+ reasons.push(
284
+ `re-rendered because the Suspense boundary above it switched from its fallback to content: React re-renders the content it kept hidden while the fallback was shown. A one-off; nothing to fix.`
285
+ );
286
+ } else if (avoidable && nothingChanged) {
212
287
  const who = input.parent ? `<${input.parent.name}> re-rendered (${describeTrigger(input.parent.trigger)})` : "its parent re-rendered";
213
- reasons.push(`re-rendered with identical props because ${who}. Wrap "${input.component}" in React.memo (or extend PureComponent).`);
288
+ reasons.push(`re-rendered with identical props because ${who}. ${memoFix}.`);
214
289
  } else if (avoidable && input.parent) {
215
290
  reasons.push(`caused by <${input.parent.name}> re-rendering (${describeTrigger(input.parent.trigger)}).`);
216
291
  }
@@ -219,9 +294,9 @@ function buildReport(input) {
219
294
  reasons.push(fix ? `${describe(c)}: ${fix}.` : `${describe(c)}.`);
220
295
  }
221
296
  const memoized = input.memoized !== false;
222
- if (avoidable && !memoized && input.propChanges.length > 0) {
297
+ if (avoidable && !memoized && !input.compiled && input.propChanges.length > 0) {
223
298
  reasons.push(
224
- `"${input.component}" is not memoized, so fixing the props alone will not stop this re-render: also wrap it in React.memo (or extend PureComponent).`
299
+ `"${input.component}" is not memoized, so fixing the props alone will not stop this re-render: also ${input.classComponent ? "extend PureComponent (or implement shouldComponentUpdate)" : "wrap it in React.memo (or extend PureComponent)"}.`
225
300
  );
226
301
  }
227
302
  for (const c of stateChanges) {
@@ -233,6 +308,8 @@ function buildReport(input) {
233
308
  const where = c.provider && c.provider.component ? ` (provided by <${c.provider.component}>)` : "";
234
309
  const keys = c.changedKeys && typeof c.totalKeys === "number" && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys ? `: only ${c.changedKeys.map((k) => `"${k}"`).join(", ")} of ${c.totalKeys} keys changed, yet every consumer re-renders. Split the context or memoize the slices consumers read` : "";
235
310
  reasons.push(`${c.path} changed${where}${keys}.`);
311
+ } else if (c.hook === "useDeferredValue" && isGenuine(c)) {
312
+ reasons.push(`${hookLabel(c)} caught up with its latest value: this is the deferred second render React schedules after the urgent one; nothing to fix.`);
236
313
  } else if (isGenuine(c)) reasons.push(`${hookLabel(c)} changed.`);
237
314
  else if (isStateHook(c))
238
315
  reasons.push(`${hookLabel(c)} was set to a value deep-equal to the current one (new reference, same contents): reuse the existing object or bail out before calling the setter.`);
@@ -263,6 +340,7 @@ function buildReport(input) {
263
340
  reasons,
264
341
  time: now()
265
342
  };
343
+ if (input.compiled) report.compiled = true;
266
344
  if (input.selfDuration !== void 0) report.selfDuration = input.selfDuration;
267
345
  if (input.treeDuration !== void 0) report.treeDuration = input.treeDuration;
268
346
  if (input.commitPriority) report.commitPriority = input.commitPriority;
@@ -300,10 +378,10 @@ var KIND_LABEL = {
300
378
  };
301
379
  function summarize(report) {
302
380
  const counts2 = /* @__PURE__ */ new Map();
303
- for (const c of [...report.propChanges, ...report.stateChanges, ...report.hookChanges]) {
381
+ for (const c of [...report.propChanges || [], ...report.stateChanges || [], ...report.hookChanges || []]) {
304
382
  counts2.set(c.kind, (counts2.get(c.kind) ?? 0) + 1);
305
383
  }
306
- const parts = [...counts2].map(([kind, n]) => `${n} ${KIND_LABEL[kind]}`);
384
+ const parts = [...counts2].map(([kind, n]) => `${n} ${KIND_LABEL[kind] || kind}`);
307
385
  if (parts.length === 0) parts.push("no changes");
308
386
  return parts.join(", ");
309
387
  }
@@ -330,7 +408,7 @@ function getState() {
330
408
  const g = globalThis;
331
409
  let s = g[KEY];
332
410
  if (!s) {
333
- s = { options: {}, enabled: false, printed: /* @__PURE__ */ new Map(), detach: null, nextInstanceId: 1, nextCommitId: 1, warnedOnce: /* @__PURE__ */ new Set(), scheduled: 0, commits: 0, overheadMs: 0, maxCommitMs: 0 };
411
+ s = { options: {}, enabled: false, printed: /* @__PURE__ */ new Map(), detach: null, nextInstanceId: 1, nextCommitId: 1, warnedOnce: /* @__PURE__ */ new Set(), scheduled: 0, commits: 0, overheadMs: 0, maxCommitMs: 0, truncated: 0 };
334
412
  g[KEY] = s;
335
413
  }
336
414
  return s;
@@ -360,12 +438,33 @@ function dispatch(report, override) {
360
438
  try {
361
439
  options.notifier(report);
362
440
  } catch (err) {
363
- (options.console ?? console).warn("[rerender-lens] notifier threw", err);
441
+ warnOnce("notifier", `notifier threw: ${String(err)}`);
364
442
  }
365
443
  }
366
444
  }
367
445
 
368
446
  // src/hookNames.ts
447
+ var NODE_COUNT = {
448
+ useState: 1,
449
+ useReducer: 1,
450
+ useRef: 1,
451
+ useMemo: 1,
452
+ useCallback: 1,
453
+ useEffect: 1,
454
+ useLayoutEffect: 1,
455
+ useInsertionEffect: 1,
456
+ useImperativeHandle: 1,
457
+ useSyncExternalStore: 2,
458
+ useTransition: 2,
459
+ useDeferredValue: 1,
460
+ useId: 1,
461
+ useOptimistic: 1,
462
+ useActionState: 3,
463
+ useFormState: 3,
464
+ useEffectEvent: 1,
465
+ useCacheRefresh: 1
466
+ };
467
+ var BUILTIN_HOOKS = /* @__PURE__ */ new Set([...Object.keys(NODE_COUNT), "useContext", "useDebugValue", "use"]);
369
468
  var noop = () => {
370
469
  };
371
470
  function customHooksFromStack(stack, componentName, marker) {
@@ -380,7 +479,7 @@ function customHooksFromStack(stack, componentName, marker) {
380
479
  continue;
381
480
  }
382
481
  if (name === componentName || name.endsWith("." + componentName)) break;
383
- if (/^use[A-Z0-9_]/.test(name)) names.push(name);
482
+ if (/^use[A-Z0-9_]/.test(name) && !BUILTIN_HOOKS.has(name)) names.push(name);
384
483
  if (names.length > 8) break;
385
484
  }
386
485
  return names;
@@ -680,6 +779,11 @@ function isMemoizedFiber(fiber) {
680
779
  }
681
780
  return false;
682
781
  }
782
+ function isCompiledFiber(fiber) {
783
+ if (fiber.tag === ClassComponent) return false;
784
+ const cache2 = fiber.updateQueue?.memoCache;
785
+ return !!cache2 && Array.isArray(cache2.data);
786
+ }
683
787
  function durationsOf(fiber) {
684
788
  if (typeof fiber.actualDuration !== "number") return null;
685
789
  const tree = fiber.actualDuration;
@@ -703,11 +807,14 @@ var counts = /* @__PURE__ */ new WeakMap();
703
807
  var ids = /* @__PURE__ */ new WeakMap();
704
808
  var fibersById = /* @__PURE__ */ new Map();
705
809
  var hasWeakRef = typeof WeakRef === "function";
810
+ var pruneAt = 5e3;
706
811
  function remember2(id, fiber) {
707
812
  if (!hasWeakRef) return;
813
+ if (fibersById.get(id)?.deref() === fiber) return;
708
814
  fibersById.set(id, new WeakRef(fiber));
709
- if (fibersById.size > 5e3) {
815
+ if (fibersById.size > pruneAt) {
710
816
  for (const [k, ref] of fibersById) if (!ref.deref()) fibersById.delete(k);
817
+ pruneAt = Math.max(5e3, fibersById.size * 2);
711
818
  }
712
819
  }
713
820
  function instanceIdOf(fiber) {
@@ -762,7 +869,7 @@ function nearestComponent(fiber) {
762
869
  }
763
870
  var INTERNAL_FRAME = /react-dom|react_jsx|jsx-(dev-)?runtime|\/react\/|node_modules\/react|react-stack-bottom-frame|react_stack_bottom_frame|scheduler/;
764
871
  function parseStackLocation(stack) {
765
- for (const line of stack.split("\n")) {
872
+ for (const line of stack.split("\n", 24)) {
766
873
  const m = /(?:at\s+(?:.*?\s+)?\(?|@)?((?:https?|file|webpack|vite|blob):[^\s()]+?):(\d+):(\d+)\)?\s*$/.exec(line.trim());
767
874
  if (!m || !m[1] || INTERNAL_FRAME.test(m[1])) continue;
768
875
  return { fileName: m[1], lineNumber: Number(m[2]), columnNumber: Number(m[3]) };
@@ -778,9 +885,16 @@ function sourceOf(fiber) {
778
885
  return out;
779
886
  }
780
887
  const st = fiber._debugStack;
781
- const text = typeof st === "string" ? st : st && typeof st.stack === "string" ? st.stack : null;
782
- return text ? parseStackLocation(text) : void 0;
888
+ if (st && typeof st === "object") {
889
+ if (sourceCache.has(st)) return sourceCache.get(st);
890
+ const text = typeof st.stack === "string" ? st.stack : null;
891
+ const out = text ? parseStackLocation(text) : void 0;
892
+ sourceCache.set(st, out);
893
+ return out;
894
+ }
895
+ return typeof st === "string" ? parseStackLocation(st) : void 0;
783
896
  }
897
+ var sourceCache = /* @__PURE__ */ new WeakMap();
784
898
  function bumpCount(fiber) {
785
899
  const prev = counts.get(fiber) ?? (fiber.alternate ? counts.get(fiber.alternate) : void 0) ?? 0;
786
900
  const n = prev + 1;
@@ -848,7 +962,8 @@ function diffHooks(fiber, alt) {
848
962
  const labels = hookLabelsFor(fiber, b);
849
963
  let i = 0;
850
964
  while (a && b) {
851
- if ((isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
965
+ const deferred = labels?.[i] === "useDeferredValue";
966
+ if ((deferred || isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
852
967
  const hook = labels?.[i] ?? hookLabel2(b);
853
968
  out.push({
854
969
  path: `${hook}#${i}`,
@@ -882,6 +997,10 @@ function diffContexts(fiber, alt) {
882
997
  let a = alt.dependencies?.firstContext ?? null;
883
998
  let b = fiber.dependencies?.firstContext ?? null;
884
999
  let i = 0;
1000
+ if (b && !("memoizedValue" in b)) {
1001
+ warnOnce("contextValues", "this React does not record context values on the fiber (React < 18): context changes are reported as plain parent re-renders.");
1002
+ return out;
1003
+ }
885
1004
  while (a && b) {
886
1005
  if (a.context === b.context && !Object.is(a.memoizedValue, b.memoizedValue)) {
887
1006
  const name = b.context.displayName ?? "Context";
@@ -914,10 +1033,26 @@ function diffContexts(fiber, alt) {
914
1033
  return out;
915
1034
  }
916
1035
  var isGenuine2 = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
1036
+ var isRefObject2 = (v) => {
1037
+ if (typeof v !== "object" || v === null) return false;
1038
+ const keys = Object.keys(v);
1039
+ return keys.length === 1 && keys[0] === "current";
1040
+ };
1041
+ function diffProps(prev, next) {
1042
+ if (prev === next) return [];
1043
+ const pr = prev.ref;
1044
+ const nr = next.ref;
1045
+ if (pr !== nr && isRefObject2(pr) && isRefObject2(nr)) {
1046
+ const { ref: _p, ...restPrev } = prev;
1047
+ const { ref: _n, ...restNext } = next;
1048
+ return [{ path: "ref", kind: "deep-equal", prev: pr, next: nr }, ...diffRecords(restPrev, restNext)];
1049
+ }
1050
+ return diffRecords(prev, next);
1051
+ }
917
1052
  function analyze(fiber, alt, trackHooks) {
918
1053
  const prevProps = alt.memoizedProps ?? {};
919
1054
  const nextProps = fiber.memoizedProps ?? {};
920
- const propChanges = prevProps === nextProps ? [] : diffRecords(prevProps, nextProps);
1055
+ const propChanges = diffProps(prevProps, nextProps);
921
1056
  let stateChanges = [];
922
1057
  let hookChanges = [];
923
1058
  if (fiber.tag === ClassComponent) {
@@ -930,7 +1065,7 @@ function analyze(fiber, alt, trackHooks) {
930
1065
  } else if (trackHooks) {
931
1066
  hookChanges = [...diffHooks(fiber, alt), ...diffContexts(fiber, alt)];
932
1067
  }
933
- const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore";
1068
+ const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore" && h.hook !== "useDeferredValue";
934
1069
  const causes = [];
935
1070
  if (propChanges.some(isGenuine2)) causes.push("props");
936
1071
  if (stateChanges.some(isGenuine2) || hookChanges.some((h) => isStateHook(h) && isGenuine2(h))) causes.push("state");
@@ -968,6 +1103,26 @@ function updatersOf(root) {
968
1103
  }
969
1104
  var lastCommit = null;
970
1105
  var EFFECT_LOOP_WINDOW_MS = 50;
1106
+ var MAX_REPORTS_PER_COMMIT = 200;
1107
+ var COMMIT_TIME_BUDGET_MS = 25;
1108
+ function isDeferredCatchUp(fiber) {
1109
+ const types = fiber._debugHookTypes;
1110
+ if (!types || !types.includes("useDeferredValue") || !fiber.alternate) return false;
1111
+ const first = fiber.memoizedState;
1112
+ if (!isHookList(first)) return false;
1113
+ const labels = hookLabelsFor(fiber, first);
1114
+ if (!labels) return false;
1115
+ let a = fiber.alternate.memoizedState;
1116
+ let b = first;
1117
+ for (let i = 0; a && b; a = a.next, b = b.next, i++) {
1118
+ if (labels[i] === "useDeferredValue" && !Object.is(a.memoizedState, b.memoizedState)) return true;
1119
+ }
1120
+ return false;
1121
+ }
1122
+ function isUnder(fiber, boundaries) {
1123
+ for (let f = fiber.return; f && f.tag !== HostRoot; f = f.return) if (f.tag === SuspenseComponent && boundaries.includes(f)) return true;
1124
+ return false;
1125
+ }
971
1126
  function onCommit(root, commitPriority) {
972
1127
  const s = getState();
973
1128
  if (!s.enabled) return;
@@ -977,7 +1132,7 @@ function onCommit(root, commitPriority) {
977
1132
  const rendered = [];
978
1133
  const renderedNames = /* @__PURE__ */ new Set();
979
1134
  let hot = false;
980
- let suspenseResolved = false;
1135
+ let resolvedBoundaries = null;
981
1136
  const stack = [root.current];
982
1137
  while (stack.length) {
983
1138
  const fiber = stack.pop();
@@ -987,7 +1142,7 @@ function onCommit(root, commitPriority) {
987
1142
  renderedNames.add(fiberName(fiber));
988
1143
  if (shouldTrack(fiberType(fiber), o)) rendered.push(fiber);
989
1144
  }
990
- if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) suspenseResolved = true;
1145
+ if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) (resolvedBoundaries ?? (resolvedBoundaries = [])).push(fiber);
991
1146
  if (!alt || fiber.child !== alt.child) {
992
1147
  for (let c = fiber.child; c; c = c.sibling) stack.push(c);
993
1148
  }
@@ -998,10 +1153,11 @@ function onCommit(root, commitPriority) {
998
1153
  const updaters = updatersOf(root);
999
1154
  let commitCause;
1000
1155
  let afterCommit;
1001
- if (lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u))) {
1156
+ const inputDriven = commitPriority === "immediate" || commitPriority === "user-blocking";
1157
+ if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u)) && !rendered.some(isDeferredCatchUp)) {
1002
1158
  commitCause = "effect-after-commit";
1003
1159
  afterCommit = lastCommit.id;
1004
- } else if (suspenseResolved) commitCause = "suspense-resolved";
1160
+ } else if (resolvedBoundaries) commitCause = "suspense-resolved";
1005
1161
  if (rendered.length === 0) {
1006
1162
  lastCommit = { id: s.nextCommitId, at, rendered: renderedNames };
1007
1163
  return;
@@ -1010,47 +1166,62 @@ function onCommit(root, commitPriority) {
1010
1166
  lastCommit = { id: commitId, at, rendered: renderedNames };
1011
1167
  const dispatcherRef = o.resolveHookNames ? getDispatcherRef() : null;
1012
1168
  const parentCache = /* @__PURE__ */ new Map();
1013
- for (const fiber of rendered) {
1014
- const alt = fiber.alternate;
1015
- const a = analyze(fiber, alt, trackHooks);
1016
- const durations = durationsOf(fiber);
1017
- let hookState = o.includeState !== false && trackHooks ? snapshotHooks(fiber) : void 0;
1018
- if (dispatcherRef && fiber.tag !== ClassComponent && (a.hookChanges.length || hookState && hookState.length)) {
1019
- const names = resolveHookNames(fiber, dispatcherRef);
1020
- if (names.size) {
1021
- for (const c of a.hookChanges) if (c.hook !== "useContext" && names.has(c.index)) c.custom = names.get(c.index);
1022
- if (hookState) hookState = hookState.map((h) => names.has(h.index) ? { ...h, custom: names.get(h.index) } : h);
1169
+ const endDiffScope = beginDiffScope();
1170
+ const deadline = at + COMMIT_TIME_BUDGET_MS;
1171
+ try {
1172
+ for (let i = 0; i < rendered.length; i++) {
1173
+ if (i >= MAX_REPORTS_PER_COMMIT || (i & 15) === 15 && nowMs() > deadline) {
1174
+ s.truncated += rendered.length - i;
1175
+ warnOnce("truncated", `a commit rendered ${rendered.length} tracked components; only ${i} were reported (see info().truncated). Narrow \`include\` or turn off includeState.`);
1176
+ break;
1177
+ }
1178
+ const fiber = rendered[i];
1179
+ const alt = fiber.alternate;
1180
+ const a = analyze(fiber, alt, trackHooks);
1181
+ const durations = durationsOf(fiber);
1182
+ let hookState = o.includeState !== false && trackHooks ? snapshotHooks(fiber) : void 0;
1183
+ if (dispatcherRef && fiber.tag !== ClassComponent && (a.hookChanges.length || hookState && hookState.length)) {
1184
+ const names = resolveHookNames(fiber, dispatcherRef);
1185
+ if (names.size) {
1186
+ for (const c of a.hookChanges) if (c.hook !== "useContext" && names.has(c.index)) c.custom = names.get(c.index);
1187
+ if (hookState) hookState = hookState.map((h) => names.has(h.index) ? { ...h, custom: names.get(h.index) } : h);
1188
+ }
1023
1189
  }
1190
+ const report = buildReport({
1191
+ component: fiberName(fiber),
1192
+ instanceId: instanceId(fiber),
1193
+ renderCount: bumpCount(fiber),
1194
+ prevProps: alt.memoizedProps ?? {},
1195
+ nextProps: fiber.memoizedProps ?? {},
1196
+ propChanges: a.propChanges,
1197
+ stateChanges: a.stateChanges,
1198
+ hookChanges: a.hookChanges,
1199
+ ...o.includeState !== false ? {
1200
+ hookState,
1201
+ contexts: trackHooks ? snapshotContexts(fiber) : void 0,
1202
+ state: fiber.tag === ClassComponent && fiber.memoizedState && typeof fiber.memoizedState === "object" ? fiber.memoizedState : void 0
1203
+ } : {},
1204
+ updaters,
1205
+ commitCause,
1206
+ afterCommit,
1207
+ key: fiber.key === null || fiber.key === void 0 ? null : String(fiber.key),
1208
+ parent: a.trigger === "parent" ? nearestRenderedAncestor(fiber, parentCache, trackHooks) : null,
1209
+ owner: ownerName(fiber),
1210
+ path: componentPath(fiber),
1211
+ memoized: isMemoizedFiber(fiber),
1212
+ compiled: isCompiledFiber(fiber),
1213
+ classComponent: fiber.tag === ClassComponent,
1214
+ revealed: resolvedBoundaries !== null && a.trigger === "parent" && isUnder(fiber, resolvedBoundaries),
1215
+ selfDuration: durations ? durations.self : void 0,
1216
+ treeDuration: durations ? durations.tree : void 0,
1217
+ commitId,
1218
+ commitPriority,
1219
+ source: sourceOf(fiber)
1220
+ });
1221
+ dispatch(report);
1024
1222
  }
1025
- const report = buildReport({
1026
- component: fiberName(fiber),
1027
- instanceId: instanceId(fiber),
1028
- renderCount: bumpCount(fiber),
1029
- prevProps: alt.memoizedProps ?? {},
1030
- nextProps: fiber.memoizedProps ?? {},
1031
- propChanges: a.propChanges,
1032
- stateChanges: a.stateChanges,
1033
- hookChanges: a.hookChanges,
1034
- ...o.includeState !== false ? {
1035
- hookState,
1036
- contexts: trackHooks ? snapshotContexts(fiber) : void 0,
1037
- state: fiber.tag === ClassComponent && fiber.memoizedState && typeof fiber.memoizedState === "object" ? fiber.memoizedState : void 0
1038
- } : {},
1039
- updaters,
1040
- commitCause,
1041
- afterCommit,
1042
- key: fiber.key === null || fiber.key === void 0 ? null : String(fiber.key),
1043
- parent: a.trigger === "parent" ? nearestRenderedAncestor(fiber, parentCache, trackHooks) : null,
1044
- owner: ownerName(fiber),
1045
- path: componentPath(fiber),
1046
- memoized: isMemoizedFiber(fiber),
1047
- selfDuration: durations ? durations.self : void 0,
1048
- treeDuration: durations ? durations.tree : void 0,
1049
- commitId,
1050
- commitPriority,
1051
- source: sourceOf(fiber)
1052
- });
1053
- dispatch(report);
1223
+ } finally {
1224
+ endDiffScope();
1054
1225
  }
1055
1226
  }
1056
1227
 
@@ -1152,38 +1323,270 @@ function useWhyRerender(name, values, options) {
1152
1323
  });
1153
1324
  }
1154
1325
 
1326
+ // src/causes.ts
1327
+ var isAncestorReport = (anc, r) => anc.path.length < r.path.length && r.path[anc.path.length] === anc.component && anc.path.every((p, i) => r.path[i] === p);
1328
+ var bump = (m, key, n = 1) => void m.set(key, (m.get(key) || 0) + n);
1329
+ function indexByComponent(reports) {
1330
+ const index = /* @__PURE__ */ new Map();
1331
+ for (const r of reports) {
1332
+ const list = index.get(r.component);
1333
+ if (list) list.push(r);
1334
+ else index.set(r.component, [r]);
1335
+ }
1336
+ return index;
1337
+ }
1338
+ function rootCauseOf(r, commitReports, index = indexByComponent(commitReports)) {
1339
+ let cur = r;
1340
+ const seen = /* @__PURE__ */ new Set([r]);
1341
+ while (cur.trigger === "parent" && cur.parent) {
1342
+ const parent = cur.parent;
1343
+ const candidates = index.get(parent.name);
1344
+ const p = candidates && candidates.find((x) => isAncestorReport(x, cur));
1345
+ if (!p || seen.has(p)) return { name: parent.name, trigger: parent.trigger, report: null };
1346
+ seen.add(p);
1347
+ cur = p;
1348
+ }
1349
+ return cur === r ? null : { name: cur.component, trigger: cur.trigger, report: cur };
1350
+ }
1351
+ function rootCausesOf(reports) {
1352
+ const roots = /* @__PURE__ */ new Map();
1353
+ const rootByReport = /* @__PURE__ */ new Map();
1354
+ const index = indexByComponent(reports);
1355
+ let avoidable = 0;
1356
+ let wasted = 0;
1357
+ for (const r of reports) {
1358
+ if (!r.avoidable) continue;
1359
+ avoidable++;
1360
+ if (typeof r.selfDuration === "number") wasted += r.selfDuration;
1361
+ const root = rootCauseOf(r, reports, index);
1362
+ const name = root ? root.name : r.parent && r.parent.name || "(unknown)";
1363
+ const trigger = root ? root.trigger : r.parent && r.parent.trigger || "parent";
1364
+ rootByReport.set(r, name);
1365
+ let agg = roots.get(name);
1366
+ if (!agg) {
1367
+ agg = { name, trigger, count: 0, components: /* @__PURE__ */ new Map() };
1368
+ roots.set(name, agg);
1369
+ }
1370
+ agg.count++;
1371
+ bump(agg.components, r.component);
1372
+ }
1373
+ return { roots: [...roots.values()].sort((a, b) => b.count - a.count), rootByReport, avoidable, wasted };
1374
+ }
1375
+ function analyzeCommit(reports) {
1376
+ const first = reports[0];
1377
+ return {
1378
+ id: first && typeof first.commitId === "number" ? first.commitId : 0,
1379
+ total: reports.length,
1380
+ ...rootCausesOf(reports),
1381
+ contexts: contextAttribution(reports),
1382
+ reports
1383
+ };
1384
+ }
1385
+ function contextAttribution(reports) {
1386
+ const byCtx = /* @__PURE__ */ new Map();
1387
+ for (const r of reports) {
1388
+ for (const c of r.hookChanges || []) {
1389
+ if (c.hook !== "useContext" && !/^useContext/.test(c.path)) continue;
1390
+ const m = /useContext\((.*)\)/.exec(c.path);
1391
+ const name = m && m[1] ? m[1] : c.path;
1392
+ let agg = byCtx.get(name);
1393
+ if (!agg) {
1394
+ agg = { name, consumers: 0, avoidable: 0, components: /* @__PURE__ */ new Map(), commits: /* @__PURE__ */ new Set(), providers: /* @__PURE__ */ new Map(), changedKeys: /* @__PURE__ */ new Set(), totalKeys: 0 };
1395
+ byCtx.set(name, agg);
1396
+ }
1397
+ agg.consumers++;
1398
+ if (AVOIDABLE_KINDS.has(c.kind)) agg.avoidable++;
1399
+ bump(agg.components, r.component);
1400
+ agg.commits.add(typeof r.commitId === "number" ? r.commitId : 0);
1401
+ if (c.provider && c.provider.component) bump(agg.providers, c.provider.component);
1402
+ if (c.changedKeys) for (const k of c.changedKeys) agg.changedKeys.add(k);
1403
+ if (typeof c.totalKeys === "number") agg.totalKeys = Math.max(agg.totalKeys, c.totalKeys);
1404
+ }
1405
+ }
1406
+ return [...byCtx.values()].sort((a, b) => b.consumers - a.consumers);
1407
+ }
1408
+ function cascadeTree(reports) {
1409
+ const root = { name: "", children: /* @__PURE__ */ new Map(), report: null };
1410
+ for (const r of reports) {
1411
+ let node = root;
1412
+ for (const seg of r.path.concat([r.component])) {
1413
+ let next = node.children.get(seg);
1414
+ if (!next) {
1415
+ next = { name: seg, children: /* @__PURE__ */ new Map(), report: null };
1416
+ node.children.set(seg, next);
1417
+ }
1418
+ node = next;
1419
+ }
1420
+ if (!node.report || r.avoidable) node.report = r;
1421
+ node.count = (node.count || 0) + 1;
1422
+ if (r.avoidable) node.avoidable = (node.avoidable || 0) + 1;
1423
+ }
1424
+ return root;
1425
+ }
1426
+ function rootCauseSummary(name, commits, analyze2) {
1427
+ const analyzeFn = analyze2 || ((_, reports) => analyzeCommit(reports));
1428
+ const out = { name, trigger: "parent", commits: [], total: 0, components: /* @__PURE__ */ new Map(), affected: [] };
1429
+ for (const [key, reports] of commits) {
1430
+ const analysis = analyzeFn(key, reports);
1431
+ const root = analysis.roots.find((x) => x.name === name);
1432
+ if (!root) continue;
1433
+ out.trigger = root.trigger;
1434
+ out.commits.push({ key, analysis, count: root.count, components: root.components });
1435
+ out.total += root.count;
1436
+ for (const [c, n] of root.components) bump(out.components, c, n);
1437
+ for (const r of reports) if (r.avoidable && analysis.rootByReport.get(r) === name) out.affected.push(r);
1438
+ }
1439
+ out.commits.reverse();
1440
+ return out;
1441
+ }
1442
+ function groupByCommit(reports) {
1443
+ const out = /* @__PURE__ */ new Map();
1444
+ for (const r of reports) {
1445
+ if (typeof r.commitId !== "number" || r.commitId <= 0) continue;
1446
+ const list = out.get(r.commitId);
1447
+ if (list) list.push(r);
1448
+ else out.set(r.commitId, [r]);
1449
+ }
1450
+ return out;
1451
+ }
1452
+ function rankRootCauses(reports) {
1453
+ const byName = /* @__PURE__ */ new Map();
1454
+ for (const commit of groupByCommit(reports).values()) {
1455
+ for (const root of rootCausesOf(commit).roots) {
1456
+ let agg = byName.get(root.name);
1457
+ if (!agg) {
1458
+ agg = { name: root.name, trigger: root.trigger, commits: 0, count: 0, components: /* @__PURE__ */ new Map() };
1459
+ byName.set(root.name, agg);
1460
+ }
1461
+ agg.commits++;
1462
+ agg.count += root.count;
1463
+ for (const [c, n] of root.components) bump(agg.components, c, n);
1464
+ }
1465
+ }
1466
+ return [...byName.values()].sort((a, b) => b.count - a.count || b.commits - a.commits || a.name.localeCompare(b.name));
1467
+ }
1468
+ var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
1469
+ function formatRootCauses(reports, limit = 5) {
1470
+ const ranked = rankRootCauses(reports);
1471
+ const lines = ranked.slice(0, limit).map((root, i) => `${String(i + 1).padStart(2)}. <${root.name}> (${root.trigger}) started ${plural(root.commits, "commit")}, ${plural(root.count, "avoidable re-render")} (${[...root.components].map(([c, n]) => `<${c}>${n > 1 ? " x" + n : ""}`).join(", ")})`);
1472
+ if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
1473
+ return lines.join("\n");
1474
+ }
1475
+
1155
1476
  // src/fixes.ts
1156
- var AVOIDABLE = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
1477
+ var MAX_FIX_REPORTS = 50;
1157
1478
  var rootOf = (path) => path.split(/[.[]/)[0] || path;
1479
+ var identifier = (name) => /^[A-Za-z_$][\w$]*$/.test(name) ? name : "value";
1480
+ function shortValue(v, max = 60) {
1481
+ let s;
1482
+ try {
1483
+ s = JSON.stringify(v);
1484
+ } catch {
1485
+ s = String(v);
1486
+ }
1487
+ if (s === void 0) s = String(v);
1488
+ return s.length > max ? s.slice(0, max - 3) + "..." : s;
1489
+ }
1158
1490
  function fixesFor(r) {
1159
1491
  const out = [];
1160
- const owner = r.owner || r.parent && r.parent.name || "?";
1161
- const changes = [...r.propChanges, ...r.stateChanges, ...r.hookChanges];
1492
+ const ownerName2 = r.owner || r.parent && r.parent.name || null;
1493
+ const propChanges = r.propChanges || [];
1494
+ const changes = [...propChanges, ...r.stateChanges || [], ...r.hookChanges || []];
1162
1495
  if (r.avoidable && (changes.length === 0 || r.memoized === false)) {
1496
+ const identical = changes.length === 0;
1163
1497
  out.push({
1164
1498
  kind: "memo",
1165
1499
  owner: r.component,
1166
1500
  target: r.component,
1167
1501
  prop: null,
1168
1502
  label: `Wrap <${r.component}> in React.memo`,
1169
- detail: changes.length === 0 ? `<${r.component}> re-rendered with identical props because <${r.parent && r.parent.name || "its parent"}> re-rendered.` : `<${r.component}> is not memoized: fixing its props alone will not stop the re-render.`
1503
+ detail: identical ? `<${r.component}> re-rendered with identical props because <${r.parent && r.parent.name || "its parent"}> re-rendered.` : `<${r.component}> is not memoized: fixing its props alone will not stop the re-render.`,
1504
+ snippet: `// ${r.component}
1505
+ import { memo } from 'react';
1506
+
1507
+ export const ${r.component} = memo(function ${r.component}(props) {
1508
+ // ...
1509
+ });
1510
+ // class components: extend PureComponent instead`
1170
1511
  });
1171
1512
  }
1172
- for (const c of r.propChanges) {
1173
- if (!AVOIDABLE.has(c.kind)) continue;
1513
+ for (const c of propChanges) {
1514
+ if (!AVOIDABLE_KINDS.has(c.kind)) continue;
1515
+ const owner = ownerName2 || "?";
1174
1516
  const root = rootOf(c.path);
1517
+ const id = identifier(root);
1175
1518
  if (root === "children" && (c.kind === "element" || c.kind === "deep-equal")) {
1176
- out.push({ kind: "children", owner, target: r.component, prop: "children", label: `memoize children of <${r.component}> in <${owner}>`, detail: `<${owner}> re-creates the children of <${r.component}> on every render.` });
1519
+ out.push({
1520
+ kind: "children",
1521
+ owner,
1522
+ target: r.component,
1523
+ prop: "children",
1524
+ label: `memoize children of <${r.component}> in <${owner}>`,
1525
+ detail: `<${owner}> re-creates the children of <${r.component}> on every render; they have the same types and props each time.`,
1526
+ snippet: `// ${owner}
1527
+ import { useMemo } from 'react';
1528
+
1529
+ const children = useMemo(() => (
1530
+ <>{/* the same elements */}</>
1531
+ ), [/* deps */]);
1532
+
1533
+ <${r.component}>{children}</${r.component}>
1534
+
1535
+ // static children: hoist them to module scope
1536
+ const STATIC = <em>hi</em>;`
1537
+ });
1177
1538
  } else if (c.kind === "function") {
1178
- out.push({ kind: "useCallback", owner, target: r.component, prop: root, label: `useCallback(${root}) in <${owner}>`, detail: `prop "${c.path}" of <${r.component}> is a new function on every render of <${owner}>.` });
1539
+ out.push({
1540
+ kind: "useCallback",
1541
+ owner,
1542
+ target: r.component,
1543
+ prop: root,
1544
+ label: `useCallback(${root}) in <${owner}>`,
1545
+ detail: `prop "${c.path}" of <${r.component}> is a new function on every render of <${owner}>.`,
1546
+ snippet: `// ${owner}
1547
+ import { useCallback } from 'react';
1548
+
1549
+ const ${id} = useCallback((/* args */) => {
1550
+ // ...
1551
+ }, [/* deps */]);
1552
+
1553
+ <${r.component} ${root}={${id}} />`
1554
+ });
1179
1555
  } else if (c.kind === "element") {
1180
- out.push({ kind: "useMemoElement", owner, target: r.component, prop: root, label: `memoize element prop ${root} in <${owner}>`, detail: `prop "${c.path}" of <${r.component}> is a new element with the same type and props on every render of <${owner}>.` });
1556
+ out.push({
1557
+ kind: "useMemoElement",
1558
+ owner,
1559
+ target: r.component,
1560
+ prop: root,
1561
+ label: `memoize element prop ${root} in <${owner}>`,
1562
+ detail: `prop "${c.path}" of <${r.component}> is a new element with the same type and props on every render of <${owner}>.`,
1563
+ snippet: `// ${owner}
1564
+ import { useMemo } from 'react';
1565
+
1566
+ const ${id} = useMemo(() => ${shortValue(c.next, 40)}, [/* deps */]);
1567
+ // or pass it as children from a component that does not re-render`
1568
+ });
1181
1569
  } else {
1182
- out.push({ kind: "useMemo", owner, target: r.component, prop: root, label: `useMemo(${root}) in <${owner}>`, detail: `prop "${c.path}" of <${r.component}> is a new ${Array.isArray(c.next) ? "array" : "object"} with the same contents on every render of <${owner}>.` });
1570
+ const isArray = Array.isArray(c.next);
1571
+ out.push({
1572
+ kind: "useMemo",
1573
+ owner,
1574
+ target: r.component,
1575
+ prop: root,
1576
+ label: `useMemo(${root}) in <${owner}>`,
1577
+ detail: `prop "${c.path}" of <${r.component}> is a new ${isArray ? "array" : "object"} with the same contents on every render of <${owner}>.`,
1578
+ snippet: `// ${owner}
1579
+ import { useMemo } from 'react';
1580
+
1581
+ const ${id} = useMemo(() => (${shortValue(c.next, 80)}), [/* deps */]);
1582
+
1583
+ // or, when it never changes, hoist it to module scope:
1584
+ const ${id.toUpperCase()} = ${shortValue(c.next, 80)};`
1585
+ });
1183
1586
  }
1184
1587
  }
1185
- for (const c of [...r.stateChanges, ...r.hookChanges]) {
1186
- const isContext = c.hook === "useContext";
1588
+ for (const c of [...r.stateChanges || [], ...r.hookChanges || []]) {
1589
+ const isContext = c.hook === "useContext" || /^useContext/.test(c.path);
1187
1590
  const ctxName = isContext ? (/useContext\((.*)\)/.exec(c.path) || [])[1] || "Context" : "";
1188
1591
  const providerOwner = isContext && c.provider && c.provider.component ? c.provider.component : null;
1189
1592
  if (isContext && c.kind === "different" && c.changedKeys && typeof c.totalKeys === "number" && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys) {
@@ -1193,20 +1596,63 @@ function fixesFor(r) {
1193
1596
  target: r.component,
1194
1597
  prop: ctxName,
1195
1598
  label: `split ${ctxName}${providerOwner ? ` in <${providerOwner}>` : ""}: only ${c.changedKeys.join(", ")} changed`,
1196
- detail: `${c.changedKeys.length} of ${c.totalKeys} keys changed in ${ctxName}, yet every consumer re-rendered.`
1599
+ detail: `${c.changedKeys.map((k) => `"${k}"`).join(", ")} of ${c.totalKeys} keys changed in ${ctxName}, yet every consumer (like <${r.component}>) re-rendered. Consumers that read the other keys re-render for nothing.`,
1600
+ snippet: `// ${providerOwner || "Provider"}
1601
+ // one context per independently-changing slice
1602
+ const ${identifier(ctxName)}Static = createContext(...);
1603
+ const ${identifier(ctxName)}${c.changedKeys.map((k) => k[0].toUpperCase() + k.slice(1)).join("")} = createContext(...);
1604
+
1605
+ // or keep one context and let consumers select a slice:
1606
+ const ${c.changedKeys[0]} = useContextSelector(${ctxName}, (v) => v.${c.changedKeys[0]});`
1197
1607
  });
1198
1608
  continue;
1199
1609
  }
1200
- if (!AVOIDABLE.has(c.kind)) continue;
1610
+ if (!AVOIDABLE_KINDS.has(c.kind)) continue;
1201
1611
  if (isContext) {
1202
- out.push({ kind: "contextValue", owner: providerOwner || `${ctxName}.Provider`, target: r.component, prop: ctxName, label: `memoize the ${ctxName} provider value${providerOwner ? ` in <${providerOwner}>` : ""}`, detail: `${ctxName} produced a new value that is deep-equal to the previous one.` });
1612
+ out.push({
1613
+ kind: "contextValue",
1614
+ owner: providerOwner || `${ctxName}.Provider`,
1615
+ target: r.component,
1616
+ prop: ctxName,
1617
+ label: `memoize the ${ctxName} provider value${providerOwner ? ` in <${providerOwner}>` : ""}`,
1618
+ detail: `<${r.component}> re-rendered because ${ctxName} produced a new value that is deep-equal to the previous one${providerOwner ? ` (Provider rendered by <${providerOwner}>)` : ""}.`,
1619
+ snippet: `// ${providerOwner || `where <${ctxName}.Provider> is rendered`}
1620
+ const value = useMemo(() => ({ /* ... */ }), [/* deps */]);
1621
+ <${ctxName}.Provider value={value}>`
1622
+ });
1203
1623
  } else if (c.hook === "useSyncExternalStore") {
1204
1624
  const chain = c.custom || [];
1205
1625
  const redux = chain.some((n) => /^use(App)?Selector$/.test(n));
1206
1626
  const zustand = !redux && chain.some((n) => /^use[A-Z]\w*Store$/.test(n) || n === "useStore" || n === "useBoundStore");
1207
- out.push({ kind: "storeSnapshot", owner: r.component, target: r.component, prop: c.path, label: redux ? `memoize the selector in <${r.component}>` : zustand ? `useShallow in <${r.component}>` : `stable getSnapshot in <${r.component}>`, detail: storeAdvice(c.custom) });
1627
+ const via = chain.length ? ` via ${chain.join(" \u203A ")}` : "";
1628
+ out.push({
1629
+ kind: "storeSnapshot",
1630
+ owner: r.component,
1631
+ target: r.component,
1632
+ prop: c.path,
1633
+ label: redux ? `memoize the selector in <${r.component}>` : zustand ? `useShallow in <${r.component}>` : `stable getSnapshot in <${r.component}>`,
1634
+ detail: redux ? `the selector${via} returns a new object on every call, so the component re-renders on every store change.` : zustand ? `the store selector${via} returns a new object on every call, so the component re-renders on every store change.` : `${c.path}${via} returned a new reference with the same contents; getSnapshot must return a cached value.`,
1635
+ snippet: redux ? `// ${r.component}
1636
+ import { shallowEqual } from 'react-redux';
1637
+ const slice = useSelector(selectSlice, shallowEqual);
1638
+ // or memoize: const selectSlice = createSelector([selectA, selectB], (a, b) => ({ a, b }));` : zustand ? `// ${r.component}
1639
+ import { useShallow } from 'zustand/react/shallow';
1640
+ const { a, b } = useStore(useShallow((s) => ({ a: s.a, b: s.b })));
1641
+ // or select a primitive: const a = useStore((s) => s.a);` : `// ${r.component}
1642
+ // getSnapshot must return the same reference while the data is unchanged
1643
+ const snapshot = useSyncExternalStore(subscribe, store.getSnapshot /* cached */);`
1644
+ });
1208
1645
  } else {
1209
- out.push({ kind: "bailout", owner: r.component, target: r.component, prop: c.path, label: `bail out before setting ${c.path} in <${r.component}>`, detail: `${c.path} was set to a value deep-equal to the current one (new reference).` });
1646
+ out.push({
1647
+ kind: "bailout",
1648
+ owner: r.component,
1649
+ target: r.component,
1650
+ prop: c.path,
1651
+ label: `bail out before setting ${c.path} in <${r.component}>`,
1652
+ detail: `${c.path} was set to a value deep-equal to the current one (new reference).`,
1653
+ snippet: `// ${r.component}
1654
+ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1655
+ });
1210
1656
  }
1211
1657
  }
1212
1658
  return out;
@@ -1220,11 +1666,12 @@ function rankFixes(reports) {
1220
1666
  const key = fixKey(f);
1221
1667
  let agg = byKey.get(key);
1222
1668
  if (!agg) {
1223
- agg = { ...f, key, count: 0, components: {} };
1669
+ agg = { ...f, key, count: 0, components: {}, reports: [] };
1224
1670
  byKey.set(key, agg);
1225
1671
  }
1226
1672
  agg.count++;
1227
1673
  agg.components[r.component] = (agg.components[r.component] || 0) + 1;
1674
+ if (agg.reports.length < MAX_FIX_REPORTS) agg.reports.push(r);
1228
1675
  }
1229
1676
  }
1230
1677
  return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
@@ -1234,6 +1681,8 @@ function formatFixes(reports, limit = 10) {
1234
1681
  if (!ranked.length) return "No avoidable re-renders.";
1235
1682
  const lines = ranked.slice(0, limit).map((f, i) => `${String(i + 1).padStart(2)}. ${f.label} (removes ${f.count}: ${Object.entries(f.components).map(([c, n]) => `<${c}>${n > 1 ? " x" + n : ""}`).join(", ")})`);
1236
1683
  if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
1684
+ const roots = formatRootCauses(reports, 5);
1685
+ if (roots) lines.push("", "Root causes:", roots);
1237
1686
  return lines.join("\n");
1238
1687
  }
1239
1688
 
@@ -1290,6 +1739,8 @@ function compareSummaries(before, after) {
1290
1739
  regressions: rows.filter((r) => r.delta > 0)
1291
1740
  };
1292
1741
  }
1742
+ var summarizeSession = (session, reports) => summarizeReports(reports, session);
1743
+ var compareSessions = compareSummaries;
1293
1744
  function formatComparison(c) {
1294
1745
  const w = Math.max(14, ...c.rows.map((r) => r.component.length + 2));
1295
1746
  const line = (label, b, a, d) => `${label.padEnd(w)} ${String(b).padStart(8)} ${String(a).padStart(8)} ${d.padStart(8)}`;
@@ -1444,13 +1895,24 @@ function highlight(target) {
1444
1895
  for (const s of l.sticky) s.remove();
1445
1896
  l.sticky = [];
1446
1897
  if (!target) return;
1447
- for (const node of target.nodes) {
1448
- const rect = node.getBoundingClientRect();
1449
- if (rect.width === 0 && rect.height === 0) continue;
1898
+ const frag = document.createDocumentFragment();
1899
+ for (const rect of measure(target.nodes)) {
1450
1900
  const b = box(rect, "#1a73e8", "rgba(26, 115, 232, 0.12)", target.label);
1451
- l.root.appendChild(b);
1901
+ frag.appendChild(b);
1452
1902
  l.sticky.push(b);
1453
1903
  }
1904
+ l.root.appendChild(frag);
1905
+ }
1906
+ var MAX_BOXES = 100;
1907
+ function measure(nodes) {
1908
+ const rects = [];
1909
+ for (const node of nodes) {
1910
+ if (rects.length >= MAX_BOXES) break;
1911
+ const rect = node.getBoundingClientRect();
1912
+ if (rect.width === 0 && rect.height === 0) continue;
1913
+ rects.push(rect);
1914
+ }
1915
+ return rects;
1454
1916
  }
1455
1917
  function clearHighlight() {
1456
1918
  highlight(null);
@@ -1458,27 +1920,34 @@ function clearHighlight() {
1458
1920
  function flash(target, duration = 500) {
1459
1921
  const l = ensureLayer();
1460
1922
  if (!l) return;
1461
- for (const node of target.nodes) {
1462
- const rect = node.getBoundingClientRect();
1463
- if (rect.width === 0 && rect.height === 0) continue;
1923
+ const frag = document.createDocumentFragment();
1924
+ const boxes = [];
1925
+ for (const rect of measure(target.nodes)) {
1464
1926
  const b = box(rect, "#d93025", "rgba(217, 48, 37, 0.15)");
1465
1927
  b.style.transition = `opacity ${duration}ms ease-out`;
1466
- l.root.appendChild(b);
1467
- requestAnimationFrame(() => {
1468
- b.style.opacity = "0";
1469
- });
1470
- setTimeout(() => b.remove(), duration + 50);
1928
+ frag.appendChild(b);
1929
+ boxes.push(b);
1471
1930
  }
1931
+ if (!boxes.length) return;
1932
+ l.root.appendChild(frag);
1933
+ requestAnimationFrame(() => {
1934
+ for (const b of boxes) b.style.opacity = "0";
1935
+ });
1936
+ setTimeout(() => {
1937
+ for (const b of boxes) b.remove();
1938
+ }, duration + 50);
1472
1939
  }
1473
1940
 
1474
1941
  // src/version.ts
1475
- var VERSION = "0.3.0" ;
1942
+ var VERSION = "0.5.0" ;
1476
1943
 
1477
1944
  // src/devtools.ts
1478
1945
  var DEVTOOLS_MARKER = "__rerenderLens";
1479
1946
  var PROTOCOL_VERSION = 2;
1480
1947
  var DEFAULT_CHANNEL = "rerender-lens";
1481
- function serialize(value, maxDepth = 6, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
1948
+ var SERIALIZE_MAX_ENTRIES = 100;
1949
+ var SERIALIZE_MAX_NODES = 2e4;
1950
+ function serialize(value, maxDepth = 4, seen = /* @__PURE__ */ new WeakSet(), depth = 0, budget = { nodes: SERIALIZE_MAX_NODES }) {
1482
1951
  if (value === null || value === void 0) return value;
1483
1952
  const t = typeof value;
1484
1953
  if (t === "string" || t === "boolean") return value;
@@ -1489,24 +1958,56 @@ function serialize(value, maxDepth = 6, seen = /* @__PURE__ */ new WeakSet(), de
1489
1958
  const obj = value;
1490
1959
  if (seen.has(obj)) return "[Circular]";
1491
1960
  if (depth >= maxDepth) return "[\u2026]";
1961
+ if (--budget.nodes < 0) return "[\u2026]";
1962
+ const next = (v) => serialize(v, maxDepth, seen, depth + 1, budget);
1492
1963
  seen.add(obj);
1493
1964
  try {
1494
1965
  if (isReactElement(obj)) {
1495
1966
  const out2 = { $type: "element", name: getDisplayName(obj.type) };
1496
1967
  if (obj.key !== null && obj.key !== void 0) out2.key = String(obj.key);
1497
- out2.props = serialize(obj.props, maxDepth, seen, depth + 1);
1968
+ out2.props = next(obj.props);
1498
1969
  return out2;
1499
1970
  }
1500
1971
  if (obj instanceof Date) return { $type: "Date", value: obj.toISOString() };
1501
1972
  if (obj instanceof RegExp) return { $type: "RegExp", value: String(obj) };
1973
+ if (ArrayBuffer.isView(obj) || obj instanceof ArrayBuffer) {
1974
+ const bin = obj;
1975
+ return { $type: bin.constructor?.name || "ArrayBuffer", length: typeof bin.length === "number" ? bin.length : bin.byteLength };
1976
+ }
1977
+ if (obj instanceof Promise) return "[Promise]";
1978
+ if (typeof Node !== "undefined" && obj instanceof Node) return typeof Element !== "undefined" && obj instanceof Element ? `<${obj.tagName.toLowerCase()}>` : `[${obj.nodeName}]`;
1979
+ if (obj === globalThis) return "[Window]";
1502
1980
  if (obj instanceof Map) {
1503
- return { $type: "Map", entries: [...obj].map(([k, v]) => [serialize(k, maxDepth, seen, depth + 1), serialize(v, maxDepth, seen, depth + 1)]) };
1981
+ const entries = [];
1982
+ for (const [k, v] of obj) {
1983
+ if (entries.length >= SERIALIZE_MAX_ENTRIES) {
1984
+ entries.push(["\u2026", `+${obj.size - SERIALIZE_MAX_ENTRIES} more`]);
1985
+ break;
1986
+ }
1987
+ entries.push([next(k), next(v)]);
1988
+ }
1989
+ return { $type: "Map", entries };
1990
+ }
1991
+ if (obj instanceof Set) {
1992
+ const values = [];
1993
+ for (const v of obj) {
1994
+ if (values.length >= SERIALIZE_MAX_ENTRIES) {
1995
+ values.push(`\u2026+${obj.size - SERIALIZE_MAX_ENTRIES} more`);
1996
+ break;
1997
+ }
1998
+ values.push(next(v));
1999
+ }
2000
+ return { $type: "Set", values };
2001
+ }
2002
+ if (Array.isArray(obj)) {
2003
+ const out2 = obj.slice(0, SERIALIZE_MAX_ENTRIES).map(next);
2004
+ if (obj.length > SERIALIZE_MAX_ENTRIES) out2.push(`\u2026+${obj.length - SERIALIZE_MAX_ENTRIES} more`);
2005
+ return out2;
1504
2006
  }
1505
- if (obj instanceof Set) return { $type: "Set", values: [...obj].map((v) => serialize(v, maxDepth, seen, depth + 1)) };
1506
- if (Array.isArray(obj)) return obj.map((v) => serialize(v, maxDepth, seen, depth + 1));
1507
- if (typeof Element !== "undefined" && obj instanceof Element) return `<${obj.tagName.toLowerCase()}>`;
1508
2007
  const out = {};
1509
- for (const k of Object.keys(obj)) out[k] = serialize(obj[k], maxDepth, seen, depth + 1);
2008
+ const keys = Object.keys(obj);
2009
+ for (const k of keys.slice(0, SERIALIZE_MAX_ENTRIES)) out[k] = next(obj[k]);
2010
+ if (keys.length > SERIALIZE_MAX_ENTRIES) out["\u2026"] = `+${keys.length - SERIALIZE_MAX_ENTRIES} more`;
1510
2011
  const proto = Object.getPrototypeOf(obj);
1511
2012
  if (proto && proto !== Object.prototype && proto.constructor?.name) out.$type = proto.constructor.name;
1512
2013
  return out;
@@ -1546,13 +2047,20 @@ function deserializeOptions(o) {
1546
2047
  }
1547
2048
  function createDevtoolsNotifier(options = {}) {
1548
2049
  const bufferSize = options.bufferSize ?? 300;
1549
- const maxDepth = options.maxDepth ?? 6;
2050
+ const maxDepth = options.maxDepth ?? 4;
1550
2051
  const target = options.target ?? (typeof window !== "undefined" ? window : void 0);
1551
2052
  const buffer = [];
1552
2053
  let seq = 0;
1553
2054
  let flashOn = options.flashAvoidable ?? false;
2055
+ let live = !target || typeof target.addEventListener !== "function";
2056
+ if (!live) {
2057
+ target.addEventListener("message", (event) => {
2058
+ const data = event.data;
2059
+ if ((event.source === target || !event.source) && data && data.__rerenderLensReady === true) live = true;
2060
+ });
2061
+ }
1554
2062
  const post = (type, payload) => {
1555
- if (!target) return;
2063
+ if (!target || type === "report" && !live) return;
1556
2064
  const msg = { [DEVTOOLS_MARKER]: true, version: PROTOCOL_VERSION, type, payload };
1557
2065
  target.postMessage(msg, "*");
1558
2066
  };
@@ -1569,10 +2077,12 @@ function createDevtoolsNotifier(options = {}) {
1569
2077
  injected: typeof window !== "undefined" && typeof window.__RERENDER_LENS_INJECTED__ === "string",
1570
2078
  scheduled: getState().scheduled,
1571
2079
  commits: getState().commits,
1572
- overhead: { totalMs: getState().overheadMs, maxCommitMs: getState().maxCommitMs }
2080
+ overhead: { totalMs: getState().overheadMs, maxCommitMs: getState().maxCommitMs },
2081
+ truncated: getState().truncated
1573
2082
  });
1574
2083
  const bridge = {
1575
2084
  replay: () => {
2085
+ live = true;
1576
2086
  post("hello", info());
1577
2087
  for (const e of buffer) post("report", e.payload);
1578
2088
  },
@@ -1761,47 +2271,63 @@ function createDevtoolsNotifier(options = {}) {
1761
2271
  };
1762
2272
  }
1763
2273
 
2274
+ exports.AVOIDABLE_KINDS = AVOIDABLE_KINDS;
1764
2275
  exports.DEFAULT_CHANNEL = DEFAULT_CHANNEL;
1765
2276
  exports.DEVTOOLS_MARKER = DEVTOOLS_MARKER;
2277
+ exports.KIND_LABEL = KIND_LABEL;
1766
2278
  exports.MARKER = MARKER;
1767
2279
  exports.PROTOCOL_VERSION = PROTOCOL_VERSION;
1768
2280
  exports.VERSION = VERSION;
2281
+ exports.analyzeCommit = analyzeCommit;
1769
2282
  exports.assertWithinBudget = assertWithinBudget;
1770
2283
  exports.avoidableCounts = avoidableCounts;
1771
2284
  exports.buildReport = buildReport;
2285
+ exports.cascadeTree = cascadeTree;
1772
2286
  exports.checkBudget = checkBudget;
1773
2287
  exports.classify = classify;
1774
2288
  exports.combineNotifiers = combineNotifiers;
2289
+ exports.compareSessions = compareSessions;
1775
2290
  exports.compareSummaries = compareSummaries;
1776
2291
  exports.configure = configure;
2292
+ exports.contextAttribution = contextAttribution;
1777
2293
  exports.createCollector = createCollector;
1778
2294
  exports.createDevtoolsNotifier = createDevtoolsNotifier;
1779
2295
  exports.customHooksFromStack = customHooksFromStack;
1780
2296
  exports.deepEqual = deepEqual;
1781
2297
  exports.deserializeOptions = deserializeOptions;
2298
+ exports.diffLeaves = diffLeaves;
1782
2299
  exports.diffRecords = diffRecords;
1783
2300
  exports.disable = disable;
1784
2301
  exports.ensureDevtoolsHook = ensureDevtoolsHook;
2302
+ exports.firstDifferentPath = firstDifferentPath;
1785
2303
  exports.fixKey = fixKey;
1786
2304
  exports.fixesFor = fixesFor;
1787
2305
  exports.formatComparison = formatComparison;
1788
2306
  exports.formatFixes = formatFixes;
2307
+ exports.formatRootCauses = formatRootCauses;
1789
2308
  exports.getDisplayName = getDisplayName;
1790
2309
  exports.getRenderers = getRenderers;
2310
+ exports.groupByCommit = groupByCommit;
1791
2311
  exports.hookLabel = hookLabel;
2312
+ exports.indexByComponent = indexByComponent;
1792
2313
  exports.init = init;
1793
2314
  exports.isEnabled = isEnabled;
1794
2315
  exports.isProductionReact = isProductionReact;
1795
2316
  exports.parseExport = parseExport;
1796
2317
  exports.printReport = printReport;
1797
2318
  exports.rankFixes = rankFixes;
2319
+ exports.rankRootCauses = rankRootCauses;
1798
2320
  exports.resolveHookNames = resolveHookNames;
2321
+ exports.rootCauseOf = rootCauseOf;
2322
+ exports.rootCauseSummary = rootCauseSummary;
1799
2323
  exports.serialize = serialize;
1800
2324
  exports.serializeOptions = serializeOptions;
2325
+ exports.shortValue = shortValue;
1801
2326
  exports.shouldTrack = shouldTrack;
1802
2327
  exports.storeAdvice = storeAdvice;
1803
2328
  exports.summarize = summarize;
1804
2329
  exports.summarizeReports = summarizeReports;
2330
+ exports.summarizeSession = summarizeSession;
1805
2331
  exports.toBudget = toBudget;
1806
2332
  exports.track = track;
1807
2333
  exports.useWhyRerender = useWhyRerender;