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