rerender-lens 0.4.0 → 0.7.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 (68) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +208 -359
  3. package/dist/{budget-LkNjGtRc.d.ts → budget-CA1MS7lB.d.cts} +20 -10
  4. package/dist/{budget-COBu7jBU.d.cts → budget-ok1VDJ4t.d.ts} +20 -10
  5. package/dist/cli.cjs +322 -53
  6. package/dist/cli.cjs.map +1 -1
  7. package/dist/cli.js +322 -53
  8. package/dist/cli.js.map +1 -1
  9. package/dist/{devtools-BkCct3cJ.d.cts → devtools-Cl5n_52v.d.ts} +17 -2
  10. package/dist/{devtools-CZebzpF6.d.ts → devtools-UwQx2VYS.d.cts} +17 -2
  11. package/dist/index.cjs +537 -49
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.d.cts +137 -8
  14. package/dist/index.d.ts +137 -8
  15. package/dist/index.js +521 -50
  16. package/dist/index.js.map +1 -1
  17. package/dist/jest-setup.cjs +1690 -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 +1688 -0
  22. package/dist/jest-setup.js.map +1 -0
  23. package/dist/jest.cjs +1777 -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 +1768 -0
  28. package/dist/jest.js.map +1 -0
  29. package/dist/{notifiers-BjjGSHpp.d.ts → notifiers-B5poAEvE.d.cts} +8 -4
  30. package/dist/{notifiers-BGQtWKfX.d.cts → notifiers-CqQW0Eum.d.ts} +8 -4
  31. package/dist/playwright.cjs +230 -28
  32. package/dist/playwright.cjs.map +1 -1
  33. package/dist/playwright.d.cts +4 -4
  34. package/dist/playwright.d.ts +4 -4
  35. package/dist/playwright.js +230 -28
  36. package/dist/playwright.js.map +1 -1
  37. package/dist/relay.cjs +71 -19
  38. package/dist/relay.cjs.map +1 -1
  39. package/dist/relay.d.cts +25 -3
  40. package/dist/relay.d.ts +25 -3
  41. package/dist/relay.js +71 -19
  42. package/dist/relay.js.map +1 -1
  43. package/dist/rerender-lens.iife.js +457 -62
  44. package/dist/runner-report-Bvqoe89M.d.ts +46 -0
  45. package/dist/runner-report-CvpDg2zt.d.cts +46 -0
  46. package/dist/setup.cjs +209 -35
  47. package/dist/setup.cjs.map +1 -1
  48. package/dist/setup.js +209 -35
  49. package/dist/setup.js.map +1 -1
  50. package/dist/{types-BzUEVkxJ.d.cts → types-DwNN5cWe.d.cts} +82 -2
  51. package/dist/{types-BzUEVkxJ.d.ts → types-DwNN5cWe.d.ts} +82 -2
  52. package/dist/vite.d.cts +1 -1
  53. package/dist/vite.d.ts +1 -1
  54. package/dist/vitest-setup.cjs +357 -44
  55. package/dist/vitest-setup.cjs.map +1 -1
  56. package/dist/vitest-setup.d.cts +4 -4
  57. package/dist/vitest-setup.d.ts +4 -4
  58. package/dist/vitest-setup.js +357 -44
  59. package/dist/vitest-setup.js.map +1 -1
  60. package/dist/vitest.cjs +420 -80
  61. package/dist/vitest.cjs.map +1 -1
  62. package/dist/vitest.d.cts +21 -39
  63. package/dist/vitest.d.ts +21 -39
  64. package/dist/vitest.js +421 -81
  65. package/dist/vitest.js.map +1 -1
  66. package/package.json +41 -4
  67. package/panel/panel.css +49 -0
  68. package/panel/panel.js +871 -353
package/dist/index.js CHANGED
@@ -125,6 +125,7 @@ function deepEqualObjects(a, b, seen) {
125
125
  }
126
126
  return false;
127
127
  }
128
+ var AVOIDABLE_KINDS = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
128
129
  function classify(prev, next) {
129
130
  if (typeof prev === "function" && typeof next === "function") {
130
131
  return prev.name === next.name && prev.toString() === next.toString() ? "function" : "different";
@@ -161,30 +162,47 @@ function diffRecords(prev, next, basePath = "") {
161
162
  if (Object.is(a, b)) continue;
162
163
  const kind = classify(a, b);
163
164
  if (kind === "different") {
164
- changes.push({ path: diffBudgetExhausted() ? 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 });
165
166
  } else {
166
167
  changes.push({ path, kind, prev: a, next: b });
167
168
  }
168
169
  }
169
170
  return changes;
170
171
  }
171
- function firstDifferentPath(a, b, path, depth = 0) {
172
- if (depth > 8) return path;
172
+ function firstDifferentPath(a, b, path = "", depth = 0) {
173
+ if (depth > 8) return path || "(value)";
173
174
  if (Array.isArray(a) && Array.isArray(b)) {
174
- if (a.length !== b.length) return `${path}.length`;
175
+ if (a.length !== b.length) return path ? `${path}.length` : "length";
175
176
  for (let i = 0; i < a.length; i++) {
176
177
  if (!deepEqual(a[i], b[i])) return firstDifferentPath(a[i], b[i], joinPath(path, i), depth + 1);
177
178
  }
178
- return path;
179
+ return null;
179
180
  }
180
181
  if (isPlainObject(a) && isPlainObject(b)) {
181
182
  const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
182
183
  for (const k of keys) {
183
184
  if (!deepEqual(a[k], b[k])) return firstDifferentPath(a[k], b[k], joinPath(path, k), depth + 1);
184
185
  }
185
- return path;
186
+ return null;
186
187
  }
187
- 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;
188
206
  }
189
207
 
190
208
  // src/report.ts
@@ -202,10 +220,12 @@ function storeAdvice(custom) {
202
220
  }
203
221
  var isGenuine = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
204
222
  var isChildren = (change) => change.path === "children";
223
+ var isRefObject = (change) => change.path === "ref" && change.kind === "deep-equal";
205
224
  function fixFor(change) {
206
225
  if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
207
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`;
208
227
  }
228
+ if (isRefObject(change)) return `create the ref once with useRef (or createRef outside the render) and pass the same object every time`;
209
229
  switch (change.kind) {
210
230
  case "function":
211
231
  return `wrap it in useCallback (or hoist it out of the parent's render)`;
@@ -221,6 +241,7 @@ function describe(change) {
221
241
  if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
222
242
  return `children are new React elements with the same types and props on every render of the parent`;
223
243
  }
244
+ if (isRefObject(change)) return `prop "ref" is a new ref object on every render (React re-attaches it each time)`;
224
245
  switch (change.kind) {
225
246
  case "deep-equal":
226
247
  return `prop "${change.path}" is a new reference but deep-equal to the previous value`;
@@ -248,11 +269,21 @@ function buildReport(input) {
248
269
  if (causes.length === 0) trigger = "parent";
249
270
  else if (causes.length === 1) trigger = causes[0];
250
271
  else trigger = "mixed";
251
- 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;
252
276
  const reasons = [];
253
- 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) {
254
285
  const who = input.parent ? `<${input.parent.name}> re-rendered (${describeTrigger(input.parent.trigger)})` : "its parent re-rendered";
255
- 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}.`);
256
287
  } else if (avoidable && input.parent) {
257
288
  reasons.push(`caused by <${input.parent.name}> re-rendering (${describeTrigger(input.parent.trigger)}).`);
258
289
  }
@@ -261,9 +292,9 @@ function buildReport(input) {
261
292
  reasons.push(fix ? `${describe(c)}: ${fix}.` : `${describe(c)}.`);
262
293
  }
263
294
  const memoized = input.memoized !== false;
264
- if (avoidable && !memoized && input.propChanges.length > 0) {
295
+ if (avoidable && !memoized && !input.compiled && input.propChanges.length > 0) {
265
296
  reasons.push(
266
- `"${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)"}.`
267
298
  );
268
299
  }
269
300
  for (const c of stateChanges) {
@@ -275,6 +306,8 @@ function buildReport(input) {
275
306
  const where = c.provider && c.provider.component ? ` (provided by <${c.provider.component}>)` : "";
276
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` : "";
277
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.`);
278
311
  } else if (isGenuine(c)) reasons.push(`${hookLabel(c)} changed.`);
279
312
  else if (isStateHook(c))
280
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.`);
@@ -305,6 +338,7 @@ function buildReport(input) {
305
338
  reasons,
306
339
  time: now()
307
340
  };
341
+ if (input.compiled) report.compiled = true;
308
342
  if (input.selfDuration !== void 0) report.selfDuration = input.selfDuration;
309
343
  if (input.treeDuration !== void 0) report.treeDuration = input.treeDuration;
310
344
  if (input.commitPriority) report.commitPriority = input.commitPriority;
@@ -342,10 +376,10 @@ var KIND_LABEL = {
342
376
  };
343
377
  function summarize(report) {
344
378
  const counts2 = /* @__PURE__ */ new Map();
345
- for (const c of [...report.propChanges, ...report.stateChanges, ...report.hookChanges]) {
379
+ for (const c of [...report.propChanges || [], ...report.stateChanges || [], ...report.hookChanges || []]) {
346
380
  counts2.set(c.kind, (counts2.get(c.kind) ?? 0) + 1);
347
381
  }
348
- const parts = [...counts2].map(([kind, n]) => `${n} ${KIND_LABEL[kind]}`);
382
+ const parts = [...counts2].map(([kind, n]) => `${n} ${KIND_LABEL[kind] || kind}`);
349
383
  if (parts.length === 0) parts.push("no changes");
350
384
  return parts.join(", ");
351
385
  }
@@ -367,16 +401,27 @@ function printReport(report, options) {
367
401
  }
368
402
 
369
403
  // src/state.ts
404
+ var MAX_SEEN_COMPONENTS = 500;
370
405
  var KEY = /* @__PURE__ */ Symbol.for("rerender-lens.state");
371
406
  function getState() {
372
407
  const g = globalThis;
373
408
  let s = g[KEY];
374
409
  if (!s) {
375
- 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 };
410
+ 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, seen: /* @__PURE__ */ new Set(), seenTracked: /* @__PURE__ */ new Set(), seenOverflow: false };
376
411
  g[KEY] = s;
377
412
  }
378
413
  return s;
379
414
  }
415
+ function noteRendered(s, name, tracked) {
416
+ if (!s.seen.has(name)) {
417
+ if (s.seen.size >= MAX_SEEN_COMPONENTS) {
418
+ s.seenOverflow = true;
419
+ return;
420
+ }
421
+ s.seen.add(name);
422
+ }
423
+ if (tracked) s.seenTracked.add(name);
424
+ }
380
425
  function warnOnce(key, message) {
381
426
  const s = getState();
382
427
  if (s.warnedOnce.has(key)) return;
@@ -408,6 +453,27 @@ function dispatch(report, override) {
408
453
  }
409
454
 
410
455
  // src/hookNames.ts
456
+ var NODE_COUNT = {
457
+ useState: 1,
458
+ useReducer: 1,
459
+ useRef: 1,
460
+ useMemo: 1,
461
+ useCallback: 1,
462
+ useEffect: 1,
463
+ useLayoutEffect: 1,
464
+ useInsertionEffect: 1,
465
+ useImperativeHandle: 1,
466
+ useSyncExternalStore: 2,
467
+ useTransition: 2,
468
+ useDeferredValue: 1,
469
+ useId: 1,
470
+ useOptimistic: 1,
471
+ useActionState: 3,
472
+ useFormState: 3,
473
+ useEffectEvent: 1,
474
+ useCacheRefresh: 1
475
+ };
476
+ var BUILTIN_HOOKS = /* @__PURE__ */ new Set([...Object.keys(NODE_COUNT), "useContext", "useDebugValue", "use"]);
411
477
  var noop = () => {
412
478
  };
413
479
  function customHooksFromStack(stack, componentName, marker) {
@@ -422,7 +488,7 @@ function customHooksFromStack(stack, componentName, marker) {
422
488
  continue;
423
489
  }
424
490
  if (name === componentName || name.endsWith("." + componentName)) break;
425
- if (/^use[A-Z0-9_]/.test(name)) names.push(name);
491
+ if (/^use[A-Z0-9_]/.test(name) && !BUILTIN_HOOKS.has(name)) names.push(name);
426
492
  if (names.length > 8) break;
427
493
  }
428
494
  return names;
@@ -722,6 +788,11 @@ function isMemoizedFiber(fiber) {
722
788
  }
723
789
  return false;
724
790
  }
791
+ function isCompiledFiber(fiber) {
792
+ if (fiber.tag === ClassComponent) return false;
793
+ const cache2 = fiber.updateQueue?.memoCache;
794
+ return !!cache2 && Array.isArray(cache2.data);
795
+ }
725
796
  function durationsOf(fiber) {
726
797
  if (typeof fiber.actualDuration !== "number") return null;
727
798
  const tree = fiber.actualDuration;
@@ -900,7 +971,8 @@ function diffHooks(fiber, alt) {
900
971
  const labels = hookLabelsFor(fiber, b);
901
972
  let i = 0;
902
973
  while (a && b) {
903
- if ((isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
974
+ const deferred = labels?.[i] === "useDeferredValue";
975
+ if ((deferred || isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
904
976
  const hook = labels?.[i] ?? hookLabel2(b);
905
977
  out.push({
906
978
  path: `${hook}#${i}`,
@@ -934,6 +1006,10 @@ function diffContexts(fiber, alt) {
934
1006
  let a = alt.dependencies?.firstContext ?? null;
935
1007
  let b = fiber.dependencies?.firstContext ?? null;
936
1008
  let i = 0;
1009
+ if (b && !("memoizedValue" in b)) {
1010
+ warnOnce("contextValues", "this React does not record context values on the fiber (React < 18): context changes are reported as plain parent re-renders.");
1011
+ return out;
1012
+ }
937
1013
  while (a && b) {
938
1014
  if (a.context === b.context && !Object.is(a.memoizedValue, b.memoizedValue)) {
939
1015
  const name = b.context.displayName ?? "Context";
@@ -966,10 +1042,26 @@ function diffContexts(fiber, alt) {
966
1042
  return out;
967
1043
  }
968
1044
  var isGenuine2 = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
1045
+ var isRefObject2 = (v) => {
1046
+ if (typeof v !== "object" || v === null) return false;
1047
+ const keys = Object.keys(v);
1048
+ return keys.length === 1 && keys[0] === "current";
1049
+ };
1050
+ function diffProps(prev, next) {
1051
+ if (prev === next) return [];
1052
+ const pr = prev.ref;
1053
+ const nr = next.ref;
1054
+ if (pr !== nr && isRefObject2(pr) && isRefObject2(nr)) {
1055
+ const { ref: _p, ...restPrev } = prev;
1056
+ const { ref: _n, ...restNext } = next;
1057
+ return [{ path: "ref", kind: "deep-equal", prev: pr, next: nr }, ...diffRecords(restPrev, restNext)];
1058
+ }
1059
+ return diffRecords(prev, next);
1060
+ }
969
1061
  function analyze(fiber, alt, trackHooks) {
970
1062
  const prevProps = alt.memoizedProps ?? {};
971
1063
  const nextProps = fiber.memoizedProps ?? {};
972
- const propChanges = prevProps === nextProps ? [] : diffRecords(prevProps, nextProps);
1064
+ const propChanges = diffProps(prevProps, nextProps);
973
1065
  let stateChanges = [];
974
1066
  let hookChanges = [];
975
1067
  if (fiber.tag === ClassComponent) {
@@ -982,7 +1074,7 @@ function analyze(fiber, alt, trackHooks) {
982
1074
  } else if (trackHooks) {
983
1075
  hookChanges = [...diffHooks(fiber, alt), ...diffContexts(fiber, alt)];
984
1076
  }
985
- const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore";
1077
+ const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore" && h.hook !== "useDeferredValue";
986
1078
  const causes = [];
987
1079
  if (propChanges.some(isGenuine2)) causes.push("props");
988
1080
  if (stateChanges.some(isGenuine2) || hookChanges.some((h) => isStateHook(h) && isGenuine2(h))) causes.push("state");
@@ -1022,6 +1114,24 @@ var lastCommit = null;
1022
1114
  var EFFECT_LOOP_WINDOW_MS = 50;
1023
1115
  var MAX_REPORTS_PER_COMMIT = 200;
1024
1116
  var COMMIT_TIME_BUDGET_MS = 25;
1117
+ function isDeferredCatchUp(fiber) {
1118
+ const types = fiber._debugHookTypes;
1119
+ if (!types || !types.includes("useDeferredValue") || !fiber.alternate) return false;
1120
+ const first = fiber.memoizedState;
1121
+ if (!isHookList(first)) return false;
1122
+ const labels = hookLabelsFor(fiber, first);
1123
+ if (!labels) return false;
1124
+ let a = fiber.alternate.memoizedState;
1125
+ let b = first;
1126
+ for (let i = 0; a && b; a = a.next, b = b.next, i++) {
1127
+ if (labels[i] === "useDeferredValue" && !Object.is(a.memoizedState, b.memoizedState)) return true;
1128
+ }
1129
+ return false;
1130
+ }
1131
+ function isUnder(fiber, boundaries) {
1132
+ for (let f = fiber.return; f && f.tag !== HostRoot; f = f.return) if (f.tag === SuspenseComponent && boundaries.includes(f)) return true;
1133
+ return false;
1134
+ }
1025
1135
  function onCommit(root, commitPriority) {
1026
1136
  const s = getState();
1027
1137
  if (!s.enabled) return;
@@ -1031,17 +1141,20 @@ function onCommit(root, commitPriority) {
1031
1141
  const rendered = [];
1032
1142
  const renderedNames = /* @__PURE__ */ new Set();
1033
1143
  let hot = false;
1034
- let suspenseResolved = false;
1144
+ let resolvedBoundaries = null;
1035
1145
  const stack = [root.current];
1036
1146
  while (stack.length) {
1037
1147
  const fiber = stack.pop();
1038
1148
  const alt = fiber.alternate;
1039
1149
  if (alt && isComponentTag(fiber.tag) && didRender(fiber)) {
1040
1150
  if (isHotSwapped(fiber, alt)) hot = true;
1041
- renderedNames.add(fiberName(fiber));
1042
- if (shouldTrack(fiberType(fiber), o)) rendered.push(fiber);
1151
+ const name = fiberName(fiber);
1152
+ renderedNames.add(name);
1153
+ const tracked = shouldTrack(fiberType(fiber), o);
1154
+ noteRendered(s, name, tracked);
1155
+ if (tracked) rendered.push(fiber);
1043
1156
  }
1044
- if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) suspenseResolved = true;
1157
+ if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) (resolvedBoundaries ?? (resolvedBoundaries = [])).push(fiber);
1045
1158
  if (!alt || fiber.child !== alt.child) {
1046
1159
  for (let c = fiber.child; c; c = c.sibling) stack.push(c);
1047
1160
  }
@@ -1053,10 +1166,10 @@ function onCommit(root, commitPriority) {
1053
1166
  let commitCause;
1054
1167
  let afterCommit;
1055
1168
  const inputDriven = commitPriority === "immediate" || commitPriority === "user-blocking";
1056
- if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u))) {
1169
+ if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u)) && !rendered.some(isDeferredCatchUp)) {
1057
1170
  commitCause = "effect-after-commit";
1058
1171
  afterCommit = lastCommit.id;
1059
- } else if (suspenseResolved) commitCause = "suspense-resolved";
1172
+ } else if (resolvedBoundaries) commitCause = "suspense-resolved";
1060
1173
  if (rendered.length === 0) {
1061
1174
  lastCommit = { id: s.nextCommitId, at, rendered: renderedNames };
1062
1175
  return;
@@ -1108,6 +1221,9 @@ function onCommit(root, commitPriority) {
1108
1221
  owner: ownerName(fiber),
1109
1222
  path: componentPath(fiber),
1110
1223
  memoized: isMemoizedFiber(fiber),
1224
+ compiled: isCompiledFiber(fiber),
1225
+ classComponent: fiber.tag === ClassComponent,
1226
+ revealed: resolvedBoundaries !== null && a.trigger === "parent" && isUnder(fiber, resolvedBoundaries),
1111
1227
  selfDuration: durations ? durations.self : void 0,
1112
1228
  treeDuration: durations ? durations.tree : void 0,
1113
1229
  commitId,
@@ -1161,10 +1277,53 @@ function shouldTrack(type, o) {
1161
1277
  if (o.trackAllMemoized) return isMemo(type) || isPureClass(type);
1162
1278
  return false;
1163
1279
  }
1280
+ var ANONYMOUS = "Anonymous";
1281
+ function explainTracking(type, o) {
1282
+ if (typeof type === "string")
1283
+ return { tracked: false, name: type, memoized: false, reason: `<${type}> is a DOM element, not a React component; only components are tracked.` };
1284
+ if (!isComponentLike(type))
1285
+ return { tracked: false, name: ANONYMOUS, memoized: false, reason: "This is not a React component type, so it is never tracked." };
1286
+ const name = getDisplayName(type);
1287
+ const memo = isMemo(type);
1288
+ const memoized = memo || isPureClass(type);
1289
+ if (o.exclude?.some((m) => matches(m, name)))
1290
+ return { tracked: false, name, memoized, reason: `<${name}> matches "exclude", which wins over every other rule.`, fix: `Remove "${name}" from the exclude list.` };
1291
+ if (hasMarker(type)) return { tracked: true, name, memoized, reason: `<${name}> is marked with track() (the ${MARKER} static).` };
1292
+ if (o.include?.some((m) => matches(m, name))) return { tracked: true, name, memoized, reason: `<${name}> matches "include".` };
1293
+ if (o.trackAllComponents) return { tracked: true, name, memoized, reason: '"Track every component" is on, so every component is tracked.' };
1294
+ if (o.trackAllMemoized && memoized)
1295
+ return { tracked: true, name, memoized, reason: `<${name}> is ${memo ? "a React.memo component" : "a PureComponent"}, and "Track every React.memo and PureComponent" is on.` };
1296
+ if (name === ANONYMOUS)
1297
+ return {
1298
+ tracked: false,
1299
+ name,
1300
+ memoized,
1301
+ reason: 'This component has no display name, so "include" and "exclude" cannot address it.',
1302
+ fix: "Give the function a name, set displayName, or mark it with track(fn, 'Name')."
1303
+ };
1304
+ if (o.trackAllMemoized)
1305
+ return {
1306
+ tracked: false,
1307
+ name,
1308
+ memoized,
1309
+ reason: `"Track every React.memo and PureComponent" only covers React.memo components and PureComponent classes, and <${name}> is neither.`,
1310
+ fix: `Add "${name}" to include, wrap it in React.memo, or turn on "Track every component".`
1311
+ };
1312
+ return {
1313
+ tracked: false,
1314
+ name,
1315
+ memoized,
1316
+ reason: `Nothing selects <${name}>: it is not marked with track(), no "include" pattern matches it, and neither "track every" option is on.`,
1317
+ fix: `Add "${name}" to include, or turn on "Track every React.memo and PureComponent" / "Track every component".`
1318
+ };
1319
+ }
1164
1320
  function init(options = {}) {
1165
1321
  const s = getState();
1166
1322
  s.options = { ...options };
1167
1323
  s.printed.clear();
1324
+ s.seen.clear();
1325
+ s.seenTracked.clear();
1326
+ s.seenOverflow = false;
1168
1327
  if (!s.enabled) {
1169
1328
  s.detach = attach();
1170
1329
  s.enabled = true;
@@ -1219,38 +1378,270 @@ function useWhyRerender(name, values, options) {
1219
1378
  });
1220
1379
  }
1221
1380
 
1381
+ // src/causes.ts
1382
+ 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);
1383
+ var bump = (m, key, n = 1) => void m.set(key, (m.get(key) || 0) + n);
1384
+ function indexByComponent(reports) {
1385
+ const index = /* @__PURE__ */ new Map();
1386
+ for (const r of reports) {
1387
+ const list = index.get(r.component);
1388
+ if (list) list.push(r);
1389
+ else index.set(r.component, [r]);
1390
+ }
1391
+ return index;
1392
+ }
1393
+ function rootCauseOf(r, commitReports, index = indexByComponent(commitReports)) {
1394
+ let cur = r;
1395
+ const seen = /* @__PURE__ */ new Set([r]);
1396
+ while (cur.trigger === "parent" && cur.parent) {
1397
+ const parent = cur.parent;
1398
+ const candidates = index.get(parent.name);
1399
+ const p = candidates && candidates.find((x) => isAncestorReport(x, cur));
1400
+ if (!p || seen.has(p)) return { name: parent.name, trigger: parent.trigger, report: null };
1401
+ seen.add(p);
1402
+ cur = p;
1403
+ }
1404
+ return cur === r ? null : { name: cur.component, trigger: cur.trigger, report: cur };
1405
+ }
1406
+ function rootCausesOf(reports) {
1407
+ const roots = /* @__PURE__ */ new Map();
1408
+ const rootByReport = /* @__PURE__ */ new Map();
1409
+ const index = indexByComponent(reports);
1410
+ let avoidable = 0;
1411
+ let wasted = 0;
1412
+ for (const r of reports) {
1413
+ if (!r.avoidable) continue;
1414
+ avoidable++;
1415
+ if (typeof r.selfDuration === "number") wasted += r.selfDuration;
1416
+ const root = rootCauseOf(r, reports, index);
1417
+ const name = root ? root.name : r.parent && r.parent.name || "(unknown)";
1418
+ const trigger = root ? root.trigger : r.parent && r.parent.trigger || "parent";
1419
+ rootByReport.set(r, name);
1420
+ let agg = roots.get(name);
1421
+ if (!agg) {
1422
+ agg = { name, trigger, count: 0, components: /* @__PURE__ */ new Map() };
1423
+ roots.set(name, agg);
1424
+ }
1425
+ agg.count++;
1426
+ bump(agg.components, r.component);
1427
+ }
1428
+ return { roots: [...roots.values()].sort((a, b) => b.count - a.count), rootByReport, avoidable, wasted };
1429
+ }
1430
+ function analyzeCommit(reports) {
1431
+ const first = reports[0];
1432
+ return {
1433
+ id: first && typeof first.commitId === "number" ? first.commitId : 0,
1434
+ total: reports.length,
1435
+ ...rootCausesOf(reports),
1436
+ contexts: contextAttribution(reports),
1437
+ reports
1438
+ };
1439
+ }
1440
+ function contextAttribution(reports) {
1441
+ const byCtx = /* @__PURE__ */ new Map();
1442
+ for (const r of reports) {
1443
+ for (const c of r.hookChanges || []) {
1444
+ if (c.hook !== "useContext" && !/^useContext/.test(c.path)) continue;
1445
+ const m = /useContext\((.*)\)/.exec(c.path);
1446
+ const name = m && m[1] ? m[1] : c.path;
1447
+ let agg = byCtx.get(name);
1448
+ if (!agg) {
1449
+ agg = { name, consumers: 0, avoidable: 0, components: /* @__PURE__ */ new Map(), commits: /* @__PURE__ */ new Set(), providers: /* @__PURE__ */ new Map(), changedKeys: /* @__PURE__ */ new Set(), totalKeys: 0 };
1450
+ byCtx.set(name, agg);
1451
+ }
1452
+ agg.consumers++;
1453
+ if (AVOIDABLE_KINDS.has(c.kind)) agg.avoidable++;
1454
+ bump(agg.components, r.component);
1455
+ agg.commits.add(typeof r.commitId === "number" ? r.commitId : 0);
1456
+ if (c.provider && c.provider.component) bump(agg.providers, c.provider.component);
1457
+ if (c.changedKeys) for (const k of c.changedKeys) agg.changedKeys.add(k);
1458
+ if (typeof c.totalKeys === "number") agg.totalKeys = Math.max(agg.totalKeys, c.totalKeys);
1459
+ }
1460
+ }
1461
+ return [...byCtx.values()].sort((a, b) => b.consumers - a.consumers);
1462
+ }
1463
+ function cascadeTree(reports) {
1464
+ const root = { name: "", children: /* @__PURE__ */ new Map(), report: null };
1465
+ for (const r of reports) {
1466
+ let node = root;
1467
+ for (const seg of r.path.concat([r.component])) {
1468
+ let next = node.children.get(seg);
1469
+ if (!next) {
1470
+ next = { name: seg, children: /* @__PURE__ */ new Map(), report: null };
1471
+ node.children.set(seg, next);
1472
+ }
1473
+ node = next;
1474
+ }
1475
+ if (!node.report || r.avoidable) node.report = r;
1476
+ node.count = (node.count || 0) + 1;
1477
+ if (r.avoidable) node.avoidable = (node.avoidable || 0) + 1;
1478
+ }
1479
+ return root;
1480
+ }
1481
+ function rootCauseSummary(name, commits, analyze2) {
1482
+ const analyzeFn = analyze2 || ((_, reports) => analyzeCommit(reports));
1483
+ const out = { name, trigger: "parent", commits: [], total: 0, components: /* @__PURE__ */ new Map(), affected: [] };
1484
+ for (const [key, reports] of commits) {
1485
+ const analysis = analyzeFn(key, reports);
1486
+ const root = analysis.roots.find((x) => x.name === name);
1487
+ if (!root) continue;
1488
+ out.trigger = root.trigger;
1489
+ out.commits.push({ key, analysis, count: root.count, components: root.components });
1490
+ out.total += root.count;
1491
+ for (const [c, n] of root.components) bump(out.components, c, n);
1492
+ for (const r of reports) if (r.avoidable && analysis.rootByReport.get(r) === name) out.affected.push(r);
1493
+ }
1494
+ out.commits.reverse();
1495
+ return out;
1496
+ }
1497
+ function groupByCommit(reports) {
1498
+ const out = /* @__PURE__ */ new Map();
1499
+ for (const r of reports) {
1500
+ if (typeof r.commitId !== "number" || r.commitId <= 0) continue;
1501
+ const list = out.get(r.commitId);
1502
+ if (list) list.push(r);
1503
+ else out.set(r.commitId, [r]);
1504
+ }
1505
+ return out;
1506
+ }
1507
+ function rankRootCauses(reports) {
1508
+ const byName = /* @__PURE__ */ new Map();
1509
+ for (const commit of groupByCommit(reports).values()) {
1510
+ for (const root of rootCausesOf(commit).roots) {
1511
+ let agg = byName.get(root.name);
1512
+ if (!agg) {
1513
+ agg = { name: root.name, trigger: root.trigger, commits: 0, count: 0, components: /* @__PURE__ */ new Map() };
1514
+ byName.set(root.name, agg);
1515
+ }
1516
+ agg.commits++;
1517
+ agg.count += root.count;
1518
+ for (const [c, n] of root.components) bump(agg.components, c, n);
1519
+ }
1520
+ }
1521
+ return [...byName.values()].sort((a, b) => b.count - a.count || b.commits - a.commits || a.name.localeCompare(b.name));
1522
+ }
1523
+ var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
1524
+ function formatRootCauses(reports, limit = 5) {
1525
+ const ranked = rankRootCauses(reports);
1526
+ 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(", ")})`);
1527
+ if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
1528
+ return lines.join("\n");
1529
+ }
1530
+
1222
1531
  // src/fixes.ts
1223
- var AVOIDABLE = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
1532
+ var MAX_FIX_REPORTS = 50;
1224
1533
  var rootOf = (path) => path.split(/[.[]/)[0] || path;
1534
+ var identifier = (name) => /^[A-Za-z_$][\w$]*$/.test(name) ? name : "value";
1535
+ function shortValue(v, max = 60) {
1536
+ let s;
1537
+ try {
1538
+ s = JSON.stringify(v);
1539
+ } catch {
1540
+ s = String(v);
1541
+ }
1542
+ if (s === void 0) s = String(v);
1543
+ return s.length > max ? s.slice(0, max - 3) + "..." : s;
1544
+ }
1225
1545
  function fixesFor(r) {
1226
1546
  const out = [];
1227
- const owner = r.owner || r.parent && r.parent.name || "?";
1228
- const changes = [...r.propChanges, ...r.stateChanges, ...r.hookChanges];
1547
+ const ownerName2 = r.owner || r.parent && r.parent.name || null;
1548
+ const propChanges = r.propChanges || [];
1549
+ const changes = [...propChanges, ...r.stateChanges || [], ...r.hookChanges || []];
1229
1550
  if (r.avoidable && (changes.length === 0 || r.memoized === false)) {
1551
+ const identical = changes.length === 0;
1230
1552
  out.push({
1231
1553
  kind: "memo",
1232
1554
  owner: r.component,
1233
1555
  target: r.component,
1234
1556
  prop: null,
1235
1557
  label: `Wrap <${r.component}> in React.memo`,
1236
- 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.`
1558
+ 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.`,
1559
+ snippet: `// ${r.component}
1560
+ import { memo } from 'react';
1561
+
1562
+ export const ${r.component} = memo(function ${r.component}(props) {
1563
+ // ...
1564
+ });
1565
+ // class components: extend PureComponent instead`
1237
1566
  });
1238
1567
  }
1239
- for (const c of r.propChanges) {
1240
- if (!AVOIDABLE.has(c.kind)) continue;
1568
+ for (const c of propChanges) {
1569
+ if (!AVOIDABLE_KINDS.has(c.kind)) continue;
1570
+ const owner = ownerName2 || "?";
1241
1571
  const root = rootOf(c.path);
1572
+ const id = identifier(root);
1242
1573
  if (root === "children" && (c.kind === "element" || c.kind === "deep-equal")) {
1243
- 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.` });
1574
+ out.push({
1575
+ kind: "children",
1576
+ owner,
1577
+ target: r.component,
1578
+ prop: "children",
1579
+ label: `memoize children of <${r.component}> in <${owner}>`,
1580
+ detail: `<${owner}> re-creates the children of <${r.component}> on every render; they have the same types and props each time.`,
1581
+ snippet: `// ${owner}
1582
+ import { useMemo } from 'react';
1583
+
1584
+ const children = useMemo(() => (
1585
+ <>{/* the same elements */}</>
1586
+ ), [/* deps */]);
1587
+
1588
+ <${r.component}>{children}</${r.component}>
1589
+
1590
+ // static children: hoist them to module scope
1591
+ const STATIC = <em>hi</em>;`
1592
+ });
1244
1593
  } else if (c.kind === "function") {
1245
- 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}>.` });
1594
+ out.push({
1595
+ kind: "useCallback",
1596
+ owner,
1597
+ target: r.component,
1598
+ prop: root,
1599
+ label: `useCallback(${root}) in <${owner}>`,
1600
+ detail: `prop "${c.path}" of <${r.component}> is a new function on every render of <${owner}>.`,
1601
+ snippet: `// ${owner}
1602
+ import { useCallback } from 'react';
1603
+
1604
+ const ${id} = useCallback((/* args */) => {
1605
+ // ...
1606
+ }, [/* deps */]);
1607
+
1608
+ <${r.component} ${root}={${id}} />`
1609
+ });
1246
1610
  } else if (c.kind === "element") {
1247
- 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}>.` });
1611
+ out.push({
1612
+ kind: "useMemoElement",
1613
+ owner,
1614
+ target: r.component,
1615
+ prop: root,
1616
+ label: `memoize element prop ${root} in <${owner}>`,
1617
+ detail: `prop "${c.path}" of <${r.component}> is a new element with the same type and props on every render of <${owner}>.`,
1618
+ snippet: `// ${owner}
1619
+ import { useMemo } from 'react';
1620
+
1621
+ const ${id} = useMemo(() => ${shortValue(c.next, 40)}, [/* deps */]);
1622
+ // or pass it as children from a component that does not re-render`
1623
+ });
1248
1624
  } else {
1249
- 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}>.` });
1625
+ const isArray = Array.isArray(c.next);
1626
+ out.push({
1627
+ kind: "useMemo",
1628
+ owner,
1629
+ target: r.component,
1630
+ prop: root,
1631
+ label: `useMemo(${root}) in <${owner}>`,
1632
+ detail: `prop "${c.path}" of <${r.component}> is a new ${isArray ? "array" : "object"} with the same contents on every render of <${owner}>.`,
1633
+ snippet: `// ${owner}
1634
+ import { useMemo } from 'react';
1635
+
1636
+ const ${id} = useMemo(() => (${shortValue(c.next, 80)}), [/* deps */]);
1637
+
1638
+ // or, when it never changes, hoist it to module scope:
1639
+ const ${id.toUpperCase()} = ${shortValue(c.next, 80)};`
1640
+ });
1250
1641
  }
1251
1642
  }
1252
- for (const c of [...r.stateChanges, ...r.hookChanges]) {
1253
- const isContext = c.hook === "useContext";
1643
+ for (const c of [...r.stateChanges || [], ...r.hookChanges || []]) {
1644
+ const isContext = c.hook === "useContext" || /^useContext/.test(c.path);
1254
1645
  const ctxName = isContext ? (/useContext\((.*)\)/.exec(c.path) || [])[1] || "Context" : "";
1255
1646
  const providerOwner = isContext && c.provider && c.provider.component ? c.provider.component : null;
1256
1647
  if (isContext && c.kind === "different" && c.changedKeys && typeof c.totalKeys === "number" && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys) {
@@ -1260,20 +1651,63 @@ function fixesFor(r) {
1260
1651
  target: r.component,
1261
1652
  prop: ctxName,
1262
1653
  label: `split ${ctxName}${providerOwner ? ` in <${providerOwner}>` : ""}: only ${c.changedKeys.join(", ")} changed`,
1263
- detail: `${c.changedKeys.length} of ${c.totalKeys} keys changed in ${ctxName}, yet every consumer re-rendered.`
1654
+ 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.`,
1655
+ snippet: `// ${providerOwner || "Provider"}
1656
+ // one context per independently-changing slice
1657
+ const ${identifier(ctxName)}Static = createContext(...);
1658
+ const ${identifier(ctxName)}${c.changedKeys.map((k) => k[0].toUpperCase() + k.slice(1)).join("")} = createContext(...);
1659
+
1660
+ // or keep one context and let consumers select a slice:
1661
+ const ${c.changedKeys[0]} = useContextSelector(${ctxName}, (v) => v.${c.changedKeys[0]});`
1264
1662
  });
1265
1663
  continue;
1266
1664
  }
1267
- if (!AVOIDABLE.has(c.kind)) continue;
1665
+ if (!AVOIDABLE_KINDS.has(c.kind)) continue;
1268
1666
  if (isContext) {
1269
- 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.` });
1667
+ out.push({
1668
+ kind: "contextValue",
1669
+ owner: providerOwner || `${ctxName}.Provider`,
1670
+ target: r.component,
1671
+ prop: ctxName,
1672
+ label: `memoize the ${ctxName} provider value${providerOwner ? ` in <${providerOwner}>` : ""}`,
1673
+ detail: `<${r.component}> re-rendered because ${ctxName} produced a new value that is deep-equal to the previous one${providerOwner ? ` (Provider rendered by <${providerOwner}>)` : ""}.`,
1674
+ snippet: `// ${providerOwner || `where <${ctxName}.Provider> is rendered`}
1675
+ const value = useMemo(() => ({ /* ... */ }), [/* deps */]);
1676
+ <${ctxName}.Provider value={value}>`
1677
+ });
1270
1678
  } else if (c.hook === "useSyncExternalStore") {
1271
1679
  const chain = c.custom || [];
1272
1680
  const redux = chain.some((n) => /^use(App)?Selector$/.test(n));
1273
1681
  const zustand = !redux && chain.some((n) => /^use[A-Z]\w*Store$/.test(n) || n === "useStore" || n === "useBoundStore");
1274
- 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) });
1682
+ const via = chain.length ? ` via ${chain.join(" \u203A ")}` : "";
1683
+ out.push({
1684
+ kind: "storeSnapshot",
1685
+ owner: r.component,
1686
+ target: r.component,
1687
+ prop: c.path,
1688
+ label: redux ? `memoize the selector in <${r.component}>` : zustand ? `useShallow in <${r.component}>` : `stable getSnapshot in <${r.component}>`,
1689
+ 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.`,
1690
+ snippet: redux ? `// ${r.component}
1691
+ import { shallowEqual } from 'react-redux';
1692
+ const slice = useSelector(selectSlice, shallowEqual);
1693
+ // or memoize: const selectSlice = createSelector([selectA, selectB], (a, b) => ({ a, b }));` : zustand ? `// ${r.component}
1694
+ import { useShallow } from 'zustand/react/shallow';
1695
+ const { a, b } = useStore(useShallow((s) => ({ a: s.a, b: s.b })));
1696
+ // or select a primitive: const a = useStore((s) => s.a);` : `// ${r.component}
1697
+ // getSnapshot must return the same reference while the data is unchanged
1698
+ const snapshot = useSyncExternalStore(subscribe, store.getSnapshot /* cached */);`
1699
+ });
1275
1700
  } else {
1276
- 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).` });
1701
+ out.push({
1702
+ kind: "bailout",
1703
+ owner: r.component,
1704
+ target: r.component,
1705
+ prop: c.path,
1706
+ label: `bail out before setting ${c.path} in <${r.component}>`,
1707
+ detail: `${c.path} was set to a value deep-equal to the current one (new reference).`,
1708
+ snippet: `// ${r.component}
1709
+ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1710
+ });
1277
1711
  }
1278
1712
  }
1279
1713
  return out;
@@ -1287,11 +1721,12 @@ function rankFixes(reports) {
1287
1721
  const key = fixKey(f);
1288
1722
  let agg = byKey.get(key);
1289
1723
  if (!agg) {
1290
- agg = { ...f, key, count: 0, components: {} };
1724
+ agg = { ...f, key, count: 0, components: {}, reports: [] };
1291
1725
  byKey.set(key, agg);
1292
1726
  }
1293
1727
  agg.count++;
1294
1728
  agg.components[r.component] = (agg.components[r.component] || 0) + 1;
1729
+ if (agg.reports.length < MAX_FIX_REPORTS) agg.reports.push(r);
1295
1730
  }
1296
1731
  }
1297
1732
  return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
@@ -1301,6 +1736,8 @@ function formatFixes(reports, limit = 10) {
1301
1736
  if (!ranked.length) return "No avoidable re-renders.";
1302
1737
  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(", ")})`);
1303
1738
  if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
1739
+ const roots = formatRootCauses(reports, 5);
1740
+ if (roots) lines.push("", "Root causes:", roots);
1304
1741
  return lines.join("\n");
1305
1742
  }
1306
1743
 
@@ -1357,6 +1794,8 @@ function compareSummaries(before, after) {
1357
1794
  regressions: rows.filter((r) => r.delta > 0)
1358
1795
  };
1359
1796
  }
1797
+ var summarizeSession = (session, reports) => summarizeReports(reports, session);
1798
+ var compareSessions = compareSummaries;
1360
1799
  function formatComparison(c) {
1361
1800
  const w = Math.max(14, ...c.rows.map((r) => r.component.length + 2));
1362
1801
  const line = (label, b, a, d) => `${label.padEnd(w)} ${String(b).padStart(8)} ${String(a).padStart(8)} ${d.padStart(8)}`;
@@ -1555,7 +1994,7 @@ function flash(target, duration = 500) {
1555
1994
  }
1556
1995
 
1557
1996
  // src/version.ts
1558
- var VERSION = "0.4.0" ;
1997
+ var VERSION = "0.7.0" ;
1559
1998
 
1560
1999
  // src/devtools.ts
1561
2000
  var DEVTOOLS_MARKER = "__rerenderLens";
@@ -1681,6 +2120,18 @@ function createDevtoolsNotifier(options = {}) {
1681
2120
  target.postMessage(msg, "*");
1682
2121
  };
1683
2122
  const source = options.source ?? "page";
2123
+ const trackingSummary = () => {
2124
+ const s = getState();
2125
+ const o = s.options;
2126
+ return {
2127
+ mode: o.trackAllComponents ? "all" : o.trackAllMemoized ? "memoized" : "marked",
2128
+ include: (o.include ?? []).map(matcherToString).filter((x) => x !== null),
2129
+ exclude: (o.exclude ?? []).map(matcherToString).filter((x) => x !== null),
2130
+ renderedCount: s.seen.size,
2131
+ trackedCount: s.seenTracked.size,
2132
+ overflow: s.seenOverflow
2133
+ };
2134
+ };
1684
2135
  const info = () => ({
1685
2136
  count: buffer.length,
1686
2137
  library: VERSION,
@@ -1694,7 +2145,8 @@ function createDevtoolsNotifier(options = {}) {
1694
2145
  scheduled: getState().scheduled,
1695
2146
  commits: getState().commits,
1696
2147
  overhead: { totalMs: getState().overheadMs, maxCommitMs: getState().maxCommitMs },
1697
- truncated: getState().truncated
2148
+ truncated: getState().truncated,
2149
+ tracking: trackingSummary()
1698
2150
  });
1699
2151
  const bridge = {
1700
2152
  replay: () => {
@@ -1740,6 +2192,10 @@ function createDevtoolsNotifier(options = {}) {
1740
2192
  flashAvoidable: (on) => {
1741
2193
  flashOn = !!on;
1742
2194
  },
2195
+ explain: (target2) => {
2196
+ const comp = typeof target2 === "number" ? fiberById(target2) : nearestComponent(fiberForNode(target2));
2197
+ return comp ? explainTracking(fiberType(comp), getState().options) : null;
2198
+ },
1743
2199
  inspect: (node) => {
1744
2200
  const comp = nearestComponent(fiberForNode(node));
1745
2201
  if (!comp) return null;
@@ -1754,6 +2210,7 @@ function createDevtoolsNotifier(options = {}) {
1754
2210
  component: fiberName(comp),
1755
2211
  instanceId: id,
1756
2212
  tracked: shouldTrack(fiberType(comp), getState().options),
2213
+ tracking: explainTracking(fiberType(comp), getState().options),
1757
2214
  path,
1758
2215
  reports: id === null ? [] : buffer.filter((e) => e.instanceId === id).slice(-20).map((e) => e.payload)
1759
2216
  };
@@ -1788,6 +2245,9 @@ function createDevtoolsNotifier(options = {}) {
1788
2245
  bridge.flashAvoidable(!!data.arg);
1789
2246
  reply.result = true;
1790
2247
  break;
2248
+ case "explain":
2249
+ reply.result = bridge.explain(data.arg);
2250
+ break;
1791
2251
  default:
1792
2252
  reply.error = `unknown command ${String(data.cmd)}`;
1793
2253
  }
@@ -1796,6 +2256,7 @@ function createDevtoolsNotifier(options = {}) {
1796
2256
  }
1797
2257
  return reply;
1798
2258
  };
2259
+ const isRelayGreeting = (data) => !!data && typeof data === "object" && data.__rerenderLensRelay === true && typeof data.app === "string";
1799
2260
  const isCommand = (data) => !!data && typeof data === "object" && data.__rerenderLensCmd === true && typeof data.id === "string";
1800
2261
  const envelope = (type, payload) => ({ [DEVTOOLS_MARKER]: true, version: PROTOCOL_VERSION, type, payload });
1801
2262
  const sinks = [];
@@ -1818,14 +2279,20 @@ function createDevtoolsNotifier(options = {}) {
1818
2279
  const relayUrl = options.relay ?? (typeof window !== "undefined" ? window.__RERENDER_LENS_RELAY__ : void 0);
1819
2280
  const ES = options.eventSource ?? (typeof EventSource === "function" ? EventSource : null);
1820
2281
  if (relayUrl && ES && typeof fetch === "function") {
1821
- const base = relayUrl.replace(/\/$/, "");
2282
+ const [rawBase, relayQuery = ""] = relayUrl.split("?");
2283
+ const base = (rawBase ?? "").replace(/\/$/, "");
2284
+ const relayToken = new URLSearchParams(relayQuery).get("token");
2285
+ const auth = relayToken ? `token=${encodeURIComponent(relayToken)}` : "";
1822
2286
  let queue = [];
1823
2287
  let scheduled = false;
2288
+ let appId = null;
1824
2289
  const flush = () => {
1825
2290
  scheduled = false;
1826
2291
  const batch = queue;
1827
2292
  queue = [];
1828
- fetch(`${base}/message`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(batch), keepalive: true }).catch(() => {
2293
+ const params = [appId ? `app=${encodeURIComponent(appId)}` : "", auth].filter(Boolean).join("&");
2294
+ const to = `${base}/message${params ? `?${params}` : ""}`;
2295
+ fetch(to, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(batch), keepalive: true }).catch(() => {
1829
2296
  });
1830
2297
  };
1831
2298
  const relaySend = (message) => {
@@ -1836,7 +2303,8 @@ function createDevtoolsNotifier(options = {}) {
1836
2303
  }
1837
2304
  };
1838
2305
  try {
1839
- const stream = new ES(`${base}/events?role=app`);
2306
+ const label = typeof location !== "undefined" ? `${location.host}${location.pathname}`.slice(0, 120) : "";
2307
+ const stream = new ES(`${base}/events?role=app${label ? `&label=${encodeURIComponent(label)}` : ""}${auth ? `&${auth}` : ""}`);
1840
2308
  stream.onmessage = (event) => {
1841
2309
  let parsed;
1842
2310
  try {
@@ -1844,7 +2312,10 @@ function createDevtoolsNotifier(options = {}) {
1844
2312
  } catch {
1845
2313
  return;
1846
2314
  }
1847
- for (const m of Array.isArray(parsed) ? parsed : [parsed]) if (isCommand(m)) relaySend(runCommand(m));
2315
+ for (const m of Array.isArray(parsed) ? parsed : [parsed]) {
2316
+ if (isRelayGreeting(m)) appId = m.app;
2317
+ else if (isCommand(m)) relaySend(runCommand(m));
2318
+ }
1848
2319
  };
1849
2320
  stream.onerror = () => {
1850
2321
  };
@@ -1887,6 +2358,6 @@ function createDevtoolsNotifier(options = {}) {
1887
2358
  };
1888
2359
  }
1889
2360
 
1890
- 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 };
2361
+ 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, explainTracking, 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 };
1891
2362
  //# sourceMappingURL=index.js.map
1892
2363
  //# sourceMappingURL=index.js.map