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.cjs CHANGED
@@ -127,6 +127,7 @@ function deepEqualObjects(a, b, seen) {
127
127
  }
128
128
  return false;
129
129
  }
130
+ var AVOIDABLE_KINDS = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
130
131
  function classify(prev, next) {
131
132
  if (typeof prev === "function" && typeof next === "function") {
132
133
  return prev.name === next.name && prev.toString() === next.toString() ? "function" : "different";
@@ -163,30 +164,47 @@ function diffRecords(prev, next, basePath = "") {
163
164
  if (Object.is(a, b)) continue;
164
165
  const kind = classify(a, b);
165
166
  if (kind === "different") {
166
- changes.push({ path: diffBudgetExhausted() ? path : firstDifferentPath(a, b, path), kind, prev: a, next: b });
167
+ changes.push({ path: diffBudgetExhausted() ? path : firstDifferentPath(a, b, path) ?? path, kind, prev: a, next: b });
167
168
  } else {
168
169
  changes.push({ path, kind, prev: a, next: b });
169
170
  }
170
171
  }
171
172
  return changes;
172
173
  }
173
- function firstDifferentPath(a, b, path, depth = 0) {
174
- if (depth > 8) return path;
174
+ function firstDifferentPath(a, b, path = "", depth = 0) {
175
+ if (depth > 8) return path || "(value)";
175
176
  if (Array.isArray(a) && Array.isArray(b)) {
176
- if (a.length !== b.length) return `${path}.length`;
177
+ if (a.length !== b.length) return path ? `${path}.length` : "length";
177
178
  for (let i = 0; i < a.length; i++) {
178
179
  if (!deepEqual(a[i], b[i])) return firstDifferentPath(a[i], b[i], joinPath(path, i), depth + 1);
179
180
  }
180
- return path;
181
+ return null;
181
182
  }
182
183
  if (isPlainObject(a) && isPlainObject(b)) {
183
184
  const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
184
185
  for (const k of keys) {
185
186
  if (!deepEqual(a[k], b[k])) return firstDifferentPath(a[k], b[k], joinPath(path, k), depth + 1);
186
187
  }
187
- return path;
188
+ return null;
188
189
  }
189
- return path;
190
+ return deepEqual(a, b) ? null : path || "(value)";
191
+ }
192
+ function diffLeaves(a, b, limit = 20, path = "", out = []) {
193
+ if (out.length >= limit || Object.is(a, b)) return out;
194
+ if (Array.isArray(a) && Array.isArray(b)) {
195
+ const n = Math.max(a.length, b.length);
196
+ for (let i = 0; i < n && out.length < limit; i++) diffLeaves(a[i], b[i], limit, joinPath(path, i), out);
197
+ return out;
198
+ }
199
+ if (isPlainObject(a) && isPlainObject(b)) {
200
+ for (const k of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) {
201
+ if (out.length >= limit) break;
202
+ diffLeaves(a[k], b[k], limit, joinPath(path, k), out);
203
+ }
204
+ return out;
205
+ }
206
+ if (!deepEqual(a, b)) out.push({ path: path || "(value)", prev: a, next: b });
207
+ return out;
190
208
  }
191
209
 
192
210
  // src/report.ts
@@ -204,10 +222,12 @@ function storeAdvice(custom) {
204
222
  }
205
223
  var isGenuine = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
206
224
  var isChildren = (change) => change.path === "children";
225
+ var isRefObject = (change) => change.path === "ref" && change.kind === "deep-equal";
207
226
  function fixFor(change) {
208
227
  if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
209
228
  return `lift the children out of the parent's render: memoize them with useMemo, hoist static elements to module scope, or render them from a component that does not re-render`;
210
229
  }
230
+ if (isRefObject(change)) return `create the ref once with useRef (or createRef outside the render) and pass the same object every time`;
211
231
  switch (change.kind) {
212
232
  case "function":
213
233
  return `wrap it in useCallback (or hoist it out of the parent's render)`;
@@ -223,6 +243,7 @@ function describe(change) {
223
243
  if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
224
244
  return `children are new React elements with the same types and props on every render of the parent`;
225
245
  }
246
+ if (isRefObject(change)) return `prop "ref" is a new ref object on every render (React re-attaches it each time)`;
226
247
  switch (change.kind) {
227
248
  case "deep-equal":
228
249
  return `prop "${change.path}" is a new reference but deep-equal to the previous value`;
@@ -250,11 +271,21 @@ function buildReport(input) {
250
271
  if (causes.length === 0) trigger = "parent";
251
272
  else if (causes.length === 1) trigger = causes[0];
252
273
  else trigger = "mixed";
253
- const avoidable = trigger === "parent";
274
+ const nothingChanged = input.propChanges.length === 0 && stateChanges.length === 0 && hookChanges.length === 0;
275
+ const cacheHit = input.compiled === true && trigger === "parent" && input.propChanges.length === 0;
276
+ const revealed = input.revealed === true && trigger === "parent";
277
+ const avoidable = trigger === "parent" && !cacheHit && !revealed;
254
278
  const reasons = [];
255
- if (avoidable && input.propChanges.length === 0 && stateChanges.length === 0 && hookChanges.length === 0) {
279
+ const memoFix = input.classComponent ? `Make "${input.component}" extend PureComponent (or implement shouldComponentUpdate)` : `Wrap "${input.component}" in React.memo (or extend PureComponent)`;
280
+ if (cacheHit) {
281
+ reasons.push(`compiled by React Compiler: its output is memoized and the inputs did not change, so this render is cheap; nothing to fix.`);
282
+ } else if (revealed) {
283
+ reasons.push(
284
+ `re-rendered because the Suspense boundary above it switched from its fallback to content: React re-renders the content it kept hidden while the fallback was shown. A one-off; nothing to fix.`
285
+ );
286
+ } else if (avoidable && nothingChanged) {
256
287
  const who = input.parent ? `<${input.parent.name}> re-rendered (${describeTrigger(input.parent.trigger)})` : "its parent re-rendered";
257
- reasons.push(`re-rendered with identical props because ${who}. Wrap "${input.component}" in React.memo (or extend PureComponent).`);
288
+ reasons.push(`re-rendered with identical props because ${who}. ${memoFix}.`);
258
289
  } else if (avoidable && input.parent) {
259
290
  reasons.push(`caused by <${input.parent.name}> re-rendering (${describeTrigger(input.parent.trigger)}).`);
260
291
  }
@@ -263,9 +294,9 @@ function buildReport(input) {
263
294
  reasons.push(fix ? `${describe(c)}: ${fix}.` : `${describe(c)}.`);
264
295
  }
265
296
  const memoized = input.memoized !== false;
266
- if (avoidable && !memoized && input.propChanges.length > 0) {
297
+ if (avoidable && !memoized && !input.compiled && input.propChanges.length > 0) {
267
298
  reasons.push(
268
- `"${input.component}" is not memoized, so fixing the props alone will not stop this re-render: also wrap it in React.memo (or extend PureComponent).`
299
+ `"${input.component}" is not memoized, so fixing the props alone will not stop this re-render: also ${input.classComponent ? "extend PureComponent (or implement shouldComponentUpdate)" : "wrap it in React.memo (or extend PureComponent)"}.`
269
300
  );
270
301
  }
271
302
  for (const c of stateChanges) {
@@ -277,6 +308,8 @@ function buildReport(input) {
277
308
  const where = c.provider && c.provider.component ? ` (provided by <${c.provider.component}>)` : "";
278
309
  const keys = c.changedKeys && typeof c.totalKeys === "number" && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys ? `: only ${c.changedKeys.map((k) => `"${k}"`).join(", ")} of ${c.totalKeys} keys changed, yet every consumer re-renders. Split the context or memoize the slices consumers read` : "";
279
310
  reasons.push(`${c.path} changed${where}${keys}.`);
311
+ } else if (c.hook === "useDeferredValue" && isGenuine(c)) {
312
+ reasons.push(`${hookLabel(c)} caught up with its latest value: this is the deferred second render React schedules after the urgent one; nothing to fix.`);
280
313
  } else if (isGenuine(c)) reasons.push(`${hookLabel(c)} changed.`);
281
314
  else if (isStateHook(c))
282
315
  reasons.push(`${hookLabel(c)} was set to a value deep-equal to the current one (new reference, same contents): reuse the existing object or bail out before calling the setter.`);
@@ -307,6 +340,7 @@ function buildReport(input) {
307
340
  reasons,
308
341
  time: now()
309
342
  };
343
+ if (input.compiled) report.compiled = true;
310
344
  if (input.selfDuration !== void 0) report.selfDuration = input.selfDuration;
311
345
  if (input.treeDuration !== void 0) report.treeDuration = input.treeDuration;
312
346
  if (input.commitPriority) report.commitPriority = input.commitPriority;
@@ -344,10 +378,10 @@ var KIND_LABEL = {
344
378
  };
345
379
  function summarize(report) {
346
380
  const counts2 = /* @__PURE__ */ new Map();
347
- for (const c of [...report.propChanges, ...report.stateChanges, ...report.hookChanges]) {
381
+ for (const c of [...report.propChanges || [], ...report.stateChanges || [], ...report.hookChanges || []]) {
348
382
  counts2.set(c.kind, (counts2.get(c.kind) ?? 0) + 1);
349
383
  }
350
- const parts = [...counts2].map(([kind, n]) => `${n} ${KIND_LABEL[kind]}`);
384
+ const parts = [...counts2].map(([kind, n]) => `${n} ${KIND_LABEL[kind] || kind}`);
351
385
  if (parts.length === 0) parts.push("no changes");
352
386
  return parts.join(", ");
353
387
  }
@@ -369,16 +403,27 @@ function printReport(report, options) {
369
403
  }
370
404
 
371
405
  // src/state.ts
406
+ var MAX_SEEN_COMPONENTS = 500;
372
407
  var KEY = /* @__PURE__ */ Symbol.for("rerender-lens.state");
373
408
  function getState() {
374
409
  const g = globalThis;
375
410
  let s = g[KEY];
376
411
  if (!s) {
377
- 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 };
412
+ 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 };
378
413
  g[KEY] = s;
379
414
  }
380
415
  return s;
381
416
  }
417
+ function noteRendered(s, name, tracked) {
418
+ if (!s.seen.has(name)) {
419
+ if (s.seen.size >= MAX_SEEN_COMPONENTS) {
420
+ s.seenOverflow = true;
421
+ return;
422
+ }
423
+ s.seen.add(name);
424
+ }
425
+ if (tracked) s.seenTracked.add(name);
426
+ }
382
427
  function warnOnce(key, message) {
383
428
  const s = getState();
384
429
  if (s.warnedOnce.has(key)) return;
@@ -410,6 +455,27 @@ function dispatch(report, override) {
410
455
  }
411
456
 
412
457
  // src/hookNames.ts
458
+ var NODE_COUNT = {
459
+ useState: 1,
460
+ useReducer: 1,
461
+ useRef: 1,
462
+ useMemo: 1,
463
+ useCallback: 1,
464
+ useEffect: 1,
465
+ useLayoutEffect: 1,
466
+ useInsertionEffect: 1,
467
+ useImperativeHandle: 1,
468
+ useSyncExternalStore: 2,
469
+ useTransition: 2,
470
+ useDeferredValue: 1,
471
+ useId: 1,
472
+ useOptimistic: 1,
473
+ useActionState: 3,
474
+ useFormState: 3,
475
+ useEffectEvent: 1,
476
+ useCacheRefresh: 1
477
+ };
478
+ var BUILTIN_HOOKS = /* @__PURE__ */ new Set([...Object.keys(NODE_COUNT), "useContext", "useDebugValue", "use"]);
413
479
  var noop = () => {
414
480
  };
415
481
  function customHooksFromStack(stack, componentName, marker) {
@@ -424,7 +490,7 @@ function customHooksFromStack(stack, componentName, marker) {
424
490
  continue;
425
491
  }
426
492
  if (name === componentName || name.endsWith("." + componentName)) break;
427
- if (/^use[A-Z0-9_]/.test(name)) names.push(name);
493
+ if (/^use[A-Z0-9_]/.test(name) && !BUILTIN_HOOKS.has(name)) names.push(name);
428
494
  if (names.length > 8) break;
429
495
  }
430
496
  return names;
@@ -724,6 +790,11 @@ function isMemoizedFiber(fiber) {
724
790
  }
725
791
  return false;
726
792
  }
793
+ function isCompiledFiber(fiber) {
794
+ if (fiber.tag === ClassComponent) return false;
795
+ const cache2 = fiber.updateQueue?.memoCache;
796
+ return !!cache2 && Array.isArray(cache2.data);
797
+ }
727
798
  function durationsOf(fiber) {
728
799
  if (typeof fiber.actualDuration !== "number") return null;
729
800
  const tree = fiber.actualDuration;
@@ -902,7 +973,8 @@ function diffHooks(fiber, alt) {
902
973
  const labels = hookLabelsFor(fiber, b);
903
974
  let i = 0;
904
975
  while (a && b) {
905
- if ((isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
976
+ const deferred = labels?.[i] === "useDeferredValue";
977
+ if ((deferred || isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
906
978
  const hook = labels?.[i] ?? hookLabel2(b);
907
979
  out.push({
908
980
  path: `${hook}#${i}`,
@@ -936,6 +1008,10 @@ function diffContexts(fiber, alt) {
936
1008
  let a = alt.dependencies?.firstContext ?? null;
937
1009
  let b = fiber.dependencies?.firstContext ?? null;
938
1010
  let i = 0;
1011
+ if (b && !("memoizedValue" in b)) {
1012
+ warnOnce("contextValues", "this React does not record context values on the fiber (React < 18): context changes are reported as plain parent re-renders.");
1013
+ return out;
1014
+ }
939
1015
  while (a && b) {
940
1016
  if (a.context === b.context && !Object.is(a.memoizedValue, b.memoizedValue)) {
941
1017
  const name = b.context.displayName ?? "Context";
@@ -968,10 +1044,26 @@ function diffContexts(fiber, alt) {
968
1044
  return out;
969
1045
  }
970
1046
  var isGenuine2 = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
1047
+ var isRefObject2 = (v) => {
1048
+ if (typeof v !== "object" || v === null) return false;
1049
+ const keys = Object.keys(v);
1050
+ return keys.length === 1 && keys[0] === "current";
1051
+ };
1052
+ function diffProps(prev, next) {
1053
+ if (prev === next) return [];
1054
+ const pr = prev.ref;
1055
+ const nr = next.ref;
1056
+ if (pr !== nr && isRefObject2(pr) && isRefObject2(nr)) {
1057
+ const { ref: _p, ...restPrev } = prev;
1058
+ const { ref: _n, ...restNext } = next;
1059
+ return [{ path: "ref", kind: "deep-equal", prev: pr, next: nr }, ...diffRecords(restPrev, restNext)];
1060
+ }
1061
+ return diffRecords(prev, next);
1062
+ }
971
1063
  function analyze(fiber, alt, trackHooks) {
972
1064
  const prevProps = alt.memoizedProps ?? {};
973
1065
  const nextProps = fiber.memoizedProps ?? {};
974
- const propChanges = prevProps === nextProps ? [] : diffRecords(prevProps, nextProps);
1066
+ const propChanges = diffProps(prevProps, nextProps);
975
1067
  let stateChanges = [];
976
1068
  let hookChanges = [];
977
1069
  if (fiber.tag === ClassComponent) {
@@ -984,7 +1076,7 @@ function analyze(fiber, alt, trackHooks) {
984
1076
  } else if (trackHooks) {
985
1077
  hookChanges = [...diffHooks(fiber, alt), ...diffContexts(fiber, alt)];
986
1078
  }
987
- const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore";
1079
+ const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore" && h.hook !== "useDeferredValue";
988
1080
  const causes = [];
989
1081
  if (propChanges.some(isGenuine2)) causes.push("props");
990
1082
  if (stateChanges.some(isGenuine2) || hookChanges.some((h) => isStateHook(h) && isGenuine2(h))) causes.push("state");
@@ -1024,6 +1116,24 @@ var lastCommit = null;
1024
1116
  var EFFECT_LOOP_WINDOW_MS = 50;
1025
1117
  var MAX_REPORTS_PER_COMMIT = 200;
1026
1118
  var COMMIT_TIME_BUDGET_MS = 25;
1119
+ function isDeferredCatchUp(fiber) {
1120
+ const types = fiber._debugHookTypes;
1121
+ if (!types || !types.includes("useDeferredValue") || !fiber.alternate) return false;
1122
+ const first = fiber.memoizedState;
1123
+ if (!isHookList(first)) return false;
1124
+ const labels = hookLabelsFor(fiber, first);
1125
+ if (!labels) return false;
1126
+ let a = fiber.alternate.memoizedState;
1127
+ let b = first;
1128
+ for (let i = 0; a && b; a = a.next, b = b.next, i++) {
1129
+ if (labels[i] === "useDeferredValue" && !Object.is(a.memoizedState, b.memoizedState)) return true;
1130
+ }
1131
+ return false;
1132
+ }
1133
+ function isUnder(fiber, boundaries) {
1134
+ for (let f = fiber.return; f && f.tag !== HostRoot; f = f.return) if (f.tag === SuspenseComponent && boundaries.includes(f)) return true;
1135
+ return false;
1136
+ }
1027
1137
  function onCommit(root, commitPriority) {
1028
1138
  const s = getState();
1029
1139
  if (!s.enabled) return;
@@ -1033,17 +1143,20 @@ function onCommit(root, commitPriority) {
1033
1143
  const rendered = [];
1034
1144
  const renderedNames = /* @__PURE__ */ new Set();
1035
1145
  let hot = false;
1036
- let suspenseResolved = false;
1146
+ let resolvedBoundaries = null;
1037
1147
  const stack = [root.current];
1038
1148
  while (stack.length) {
1039
1149
  const fiber = stack.pop();
1040
1150
  const alt = fiber.alternate;
1041
1151
  if (alt && isComponentTag(fiber.tag) && didRender(fiber)) {
1042
1152
  if (isHotSwapped(fiber, alt)) hot = true;
1043
- renderedNames.add(fiberName(fiber));
1044
- if (shouldTrack(fiberType(fiber), o)) rendered.push(fiber);
1153
+ const name = fiberName(fiber);
1154
+ renderedNames.add(name);
1155
+ const tracked = shouldTrack(fiberType(fiber), o);
1156
+ noteRendered(s, name, tracked);
1157
+ if (tracked) rendered.push(fiber);
1045
1158
  }
1046
- if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) suspenseResolved = true;
1159
+ if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) (resolvedBoundaries ?? (resolvedBoundaries = [])).push(fiber);
1047
1160
  if (!alt || fiber.child !== alt.child) {
1048
1161
  for (let c = fiber.child; c; c = c.sibling) stack.push(c);
1049
1162
  }
@@ -1055,10 +1168,10 @@ function onCommit(root, commitPriority) {
1055
1168
  let commitCause;
1056
1169
  let afterCommit;
1057
1170
  const inputDriven = commitPriority === "immediate" || commitPriority === "user-blocking";
1058
- if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u))) {
1171
+ if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u)) && !rendered.some(isDeferredCatchUp)) {
1059
1172
  commitCause = "effect-after-commit";
1060
1173
  afterCommit = lastCommit.id;
1061
- } else if (suspenseResolved) commitCause = "suspense-resolved";
1174
+ } else if (resolvedBoundaries) commitCause = "suspense-resolved";
1062
1175
  if (rendered.length === 0) {
1063
1176
  lastCommit = { id: s.nextCommitId, at, rendered: renderedNames };
1064
1177
  return;
@@ -1110,6 +1223,9 @@ function onCommit(root, commitPriority) {
1110
1223
  owner: ownerName(fiber),
1111
1224
  path: componentPath(fiber),
1112
1225
  memoized: isMemoizedFiber(fiber),
1226
+ compiled: isCompiledFiber(fiber),
1227
+ classComponent: fiber.tag === ClassComponent,
1228
+ revealed: resolvedBoundaries !== null && a.trigger === "parent" && isUnder(fiber, resolvedBoundaries),
1113
1229
  selfDuration: durations ? durations.self : void 0,
1114
1230
  treeDuration: durations ? durations.tree : void 0,
1115
1231
  commitId,
@@ -1163,10 +1279,53 @@ function shouldTrack(type, o) {
1163
1279
  if (o.trackAllMemoized) return isMemo(type) || isPureClass(type);
1164
1280
  return false;
1165
1281
  }
1282
+ var ANONYMOUS = "Anonymous";
1283
+ function explainTracking(type, o) {
1284
+ if (typeof type === "string")
1285
+ return { tracked: false, name: type, memoized: false, reason: `<${type}> is a DOM element, not a React component; only components are tracked.` };
1286
+ if (!isComponentLike(type))
1287
+ return { tracked: false, name: ANONYMOUS, memoized: false, reason: "This is not a React component type, so it is never tracked." };
1288
+ const name = getDisplayName(type);
1289
+ const memo = isMemo(type);
1290
+ const memoized = memo || isPureClass(type);
1291
+ if (o.exclude?.some((m) => matches(m, name)))
1292
+ return { tracked: false, name, memoized, reason: `<${name}> matches "exclude", which wins over every other rule.`, fix: `Remove "${name}" from the exclude list.` };
1293
+ if (hasMarker(type)) return { tracked: true, name, memoized, reason: `<${name}> is marked with track() (the ${MARKER} static).` };
1294
+ if (o.include?.some((m) => matches(m, name))) return { tracked: true, name, memoized, reason: `<${name}> matches "include".` };
1295
+ if (o.trackAllComponents) return { tracked: true, name, memoized, reason: '"Track every component" is on, so every component is tracked.' };
1296
+ if (o.trackAllMemoized && memoized)
1297
+ return { tracked: true, name, memoized, reason: `<${name}> is ${memo ? "a React.memo component" : "a PureComponent"}, and "Track every React.memo and PureComponent" is on.` };
1298
+ if (name === ANONYMOUS)
1299
+ return {
1300
+ tracked: false,
1301
+ name,
1302
+ memoized,
1303
+ reason: 'This component has no display name, so "include" and "exclude" cannot address it.',
1304
+ fix: "Give the function a name, set displayName, or mark it with track(fn, 'Name')."
1305
+ };
1306
+ if (o.trackAllMemoized)
1307
+ return {
1308
+ tracked: false,
1309
+ name,
1310
+ memoized,
1311
+ reason: `"Track every React.memo and PureComponent" only covers React.memo components and PureComponent classes, and <${name}> is neither.`,
1312
+ fix: `Add "${name}" to include, wrap it in React.memo, or turn on "Track every component".`
1313
+ };
1314
+ return {
1315
+ tracked: false,
1316
+ name,
1317
+ memoized,
1318
+ reason: `Nothing selects <${name}>: it is not marked with track(), no "include" pattern matches it, and neither "track every" option is on.`,
1319
+ fix: `Add "${name}" to include, or turn on "Track every React.memo and PureComponent" / "Track every component".`
1320
+ };
1321
+ }
1166
1322
  function init(options = {}) {
1167
1323
  const s = getState();
1168
1324
  s.options = { ...options };
1169
1325
  s.printed.clear();
1326
+ s.seen.clear();
1327
+ s.seenTracked.clear();
1328
+ s.seenOverflow = false;
1170
1329
  if (!s.enabled) {
1171
1330
  s.detach = attach();
1172
1331
  s.enabled = true;
@@ -1221,38 +1380,270 @@ function useWhyRerender(name, values, options) {
1221
1380
  });
1222
1381
  }
1223
1382
 
1383
+ // src/causes.ts
1384
+ 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);
1385
+ var bump = (m, key, n = 1) => void m.set(key, (m.get(key) || 0) + n);
1386
+ function indexByComponent(reports) {
1387
+ const index = /* @__PURE__ */ new Map();
1388
+ for (const r of reports) {
1389
+ const list = index.get(r.component);
1390
+ if (list) list.push(r);
1391
+ else index.set(r.component, [r]);
1392
+ }
1393
+ return index;
1394
+ }
1395
+ function rootCauseOf(r, commitReports, index = indexByComponent(commitReports)) {
1396
+ let cur = r;
1397
+ const seen = /* @__PURE__ */ new Set([r]);
1398
+ while (cur.trigger === "parent" && cur.parent) {
1399
+ const parent = cur.parent;
1400
+ const candidates = index.get(parent.name);
1401
+ const p = candidates && candidates.find((x) => isAncestorReport(x, cur));
1402
+ if (!p || seen.has(p)) return { name: parent.name, trigger: parent.trigger, report: null };
1403
+ seen.add(p);
1404
+ cur = p;
1405
+ }
1406
+ return cur === r ? null : { name: cur.component, trigger: cur.trigger, report: cur };
1407
+ }
1408
+ function rootCausesOf(reports) {
1409
+ const roots = /* @__PURE__ */ new Map();
1410
+ const rootByReport = /* @__PURE__ */ new Map();
1411
+ const index = indexByComponent(reports);
1412
+ let avoidable = 0;
1413
+ let wasted = 0;
1414
+ for (const r of reports) {
1415
+ if (!r.avoidable) continue;
1416
+ avoidable++;
1417
+ if (typeof r.selfDuration === "number") wasted += r.selfDuration;
1418
+ const root = rootCauseOf(r, reports, index);
1419
+ const name = root ? root.name : r.parent && r.parent.name || "(unknown)";
1420
+ const trigger = root ? root.trigger : r.parent && r.parent.trigger || "parent";
1421
+ rootByReport.set(r, name);
1422
+ let agg = roots.get(name);
1423
+ if (!agg) {
1424
+ agg = { name, trigger, count: 0, components: /* @__PURE__ */ new Map() };
1425
+ roots.set(name, agg);
1426
+ }
1427
+ agg.count++;
1428
+ bump(agg.components, r.component);
1429
+ }
1430
+ return { roots: [...roots.values()].sort((a, b) => b.count - a.count), rootByReport, avoidable, wasted };
1431
+ }
1432
+ function analyzeCommit(reports) {
1433
+ const first = reports[0];
1434
+ return {
1435
+ id: first && typeof first.commitId === "number" ? first.commitId : 0,
1436
+ total: reports.length,
1437
+ ...rootCausesOf(reports),
1438
+ contexts: contextAttribution(reports),
1439
+ reports
1440
+ };
1441
+ }
1442
+ function contextAttribution(reports) {
1443
+ const byCtx = /* @__PURE__ */ new Map();
1444
+ for (const r of reports) {
1445
+ for (const c of r.hookChanges || []) {
1446
+ if (c.hook !== "useContext" && !/^useContext/.test(c.path)) continue;
1447
+ const m = /useContext\((.*)\)/.exec(c.path);
1448
+ const name = m && m[1] ? m[1] : c.path;
1449
+ let agg = byCtx.get(name);
1450
+ if (!agg) {
1451
+ agg = { name, consumers: 0, avoidable: 0, components: /* @__PURE__ */ new Map(), commits: /* @__PURE__ */ new Set(), providers: /* @__PURE__ */ new Map(), changedKeys: /* @__PURE__ */ new Set(), totalKeys: 0 };
1452
+ byCtx.set(name, agg);
1453
+ }
1454
+ agg.consumers++;
1455
+ if (AVOIDABLE_KINDS.has(c.kind)) agg.avoidable++;
1456
+ bump(agg.components, r.component);
1457
+ agg.commits.add(typeof r.commitId === "number" ? r.commitId : 0);
1458
+ if (c.provider && c.provider.component) bump(agg.providers, c.provider.component);
1459
+ if (c.changedKeys) for (const k of c.changedKeys) agg.changedKeys.add(k);
1460
+ if (typeof c.totalKeys === "number") agg.totalKeys = Math.max(agg.totalKeys, c.totalKeys);
1461
+ }
1462
+ }
1463
+ return [...byCtx.values()].sort((a, b) => b.consumers - a.consumers);
1464
+ }
1465
+ function cascadeTree(reports) {
1466
+ const root = { name: "", children: /* @__PURE__ */ new Map(), report: null };
1467
+ for (const r of reports) {
1468
+ let node = root;
1469
+ for (const seg of r.path.concat([r.component])) {
1470
+ let next = node.children.get(seg);
1471
+ if (!next) {
1472
+ next = { name: seg, children: /* @__PURE__ */ new Map(), report: null };
1473
+ node.children.set(seg, next);
1474
+ }
1475
+ node = next;
1476
+ }
1477
+ if (!node.report || r.avoidable) node.report = r;
1478
+ node.count = (node.count || 0) + 1;
1479
+ if (r.avoidable) node.avoidable = (node.avoidable || 0) + 1;
1480
+ }
1481
+ return root;
1482
+ }
1483
+ function rootCauseSummary(name, commits, analyze2) {
1484
+ const analyzeFn = analyze2 || ((_, reports) => analyzeCommit(reports));
1485
+ const out = { name, trigger: "parent", commits: [], total: 0, components: /* @__PURE__ */ new Map(), affected: [] };
1486
+ for (const [key, reports] of commits) {
1487
+ const analysis = analyzeFn(key, reports);
1488
+ const root = analysis.roots.find((x) => x.name === name);
1489
+ if (!root) continue;
1490
+ out.trigger = root.trigger;
1491
+ out.commits.push({ key, analysis, count: root.count, components: root.components });
1492
+ out.total += root.count;
1493
+ for (const [c, n] of root.components) bump(out.components, c, n);
1494
+ for (const r of reports) if (r.avoidable && analysis.rootByReport.get(r) === name) out.affected.push(r);
1495
+ }
1496
+ out.commits.reverse();
1497
+ return out;
1498
+ }
1499
+ function groupByCommit(reports) {
1500
+ const out = /* @__PURE__ */ new Map();
1501
+ for (const r of reports) {
1502
+ if (typeof r.commitId !== "number" || r.commitId <= 0) continue;
1503
+ const list = out.get(r.commitId);
1504
+ if (list) list.push(r);
1505
+ else out.set(r.commitId, [r]);
1506
+ }
1507
+ return out;
1508
+ }
1509
+ function rankRootCauses(reports) {
1510
+ const byName = /* @__PURE__ */ new Map();
1511
+ for (const commit of groupByCommit(reports).values()) {
1512
+ for (const root of rootCausesOf(commit).roots) {
1513
+ let agg = byName.get(root.name);
1514
+ if (!agg) {
1515
+ agg = { name: root.name, trigger: root.trigger, commits: 0, count: 0, components: /* @__PURE__ */ new Map() };
1516
+ byName.set(root.name, agg);
1517
+ }
1518
+ agg.commits++;
1519
+ agg.count += root.count;
1520
+ for (const [c, n] of root.components) bump(agg.components, c, n);
1521
+ }
1522
+ }
1523
+ return [...byName.values()].sort((a, b) => b.count - a.count || b.commits - a.commits || a.name.localeCompare(b.name));
1524
+ }
1525
+ var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
1526
+ function formatRootCauses(reports, limit = 5) {
1527
+ const ranked = rankRootCauses(reports);
1528
+ 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(", ")})`);
1529
+ if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
1530
+ return lines.join("\n");
1531
+ }
1532
+
1224
1533
  // src/fixes.ts
1225
- var AVOIDABLE = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
1534
+ var MAX_FIX_REPORTS = 50;
1226
1535
  var rootOf = (path) => path.split(/[.[]/)[0] || path;
1536
+ var identifier = (name) => /^[A-Za-z_$][\w$]*$/.test(name) ? name : "value";
1537
+ function shortValue(v, max = 60) {
1538
+ let s;
1539
+ try {
1540
+ s = JSON.stringify(v);
1541
+ } catch {
1542
+ s = String(v);
1543
+ }
1544
+ if (s === void 0) s = String(v);
1545
+ return s.length > max ? s.slice(0, max - 3) + "..." : s;
1546
+ }
1227
1547
  function fixesFor(r) {
1228
1548
  const out = [];
1229
- const owner = r.owner || r.parent && r.parent.name || "?";
1230
- const changes = [...r.propChanges, ...r.stateChanges, ...r.hookChanges];
1549
+ const ownerName2 = r.owner || r.parent && r.parent.name || null;
1550
+ const propChanges = r.propChanges || [];
1551
+ const changes = [...propChanges, ...r.stateChanges || [], ...r.hookChanges || []];
1231
1552
  if (r.avoidable && (changes.length === 0 || r.memoized === false)) {
1553
+ const identical = changes.length === 0;
1232
1554
  out.push({
1233
1555
  kind: "memo",
1234
1556
  owner: r.component,
1235
1557
  target: r.component,
1236
1558
  prop: null,
1237
1559
  label: `Wrap <${r.component}> in React.memo`,
1238
- 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.`
1560
+ 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.`,
1561
+ snippet: `// ${r.component}
1562
+ import { memo } from 'react';
1563
+
1564
+ export const ${r.component} = memo(function ${r.component}(props) {
1565
+ // ...
1566
+ });
1567
+ // class components: extend PureComponent instead`
1239
1568
  });
1240
1569
  }
1241
- for (const c of r.propChanges) {
1242
- if (!AVOIDABLE.has(c.kind)) continue;
1570
+ for (const c of propChanges) {
1571
+ if (!AVOIDABLE_KINDS.has(c.kind)) continue;
1572
+ const owner = ownerName2 || "?";
1243
1573
  const root = rootOf(c.path);
1574
+ const id = identifier(root);
1244
1575
  if (root === "children" && (c.kind === "element" || c.kind === "deep-equal")) {
1245
- 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.` });
1576
+ out.push({
1577
+ kind: "children",
1578
+ owner,
1579
+ target: r.component,
1580
+ prop: "children",
1581
+ label: `memoize children of <${r.component}> in <${owner}>`,
1582
+ detail: `<${owner}> re-creates the children of <${r.component}> on every render; they have the same types and props each time.`,
1583
+ snippet: `// ${owner}
1584
+ import { useMemo } from 'react';
1585
+
1586
+ const children = useMemo(() => (
1587
+ <>{/* the same elements */}</>
1588
+ ), [/* deps */]);
1589
+
1590
+ <${r.component}>{children}</${r.component}>
1591
+
1592
+ // static children: hoist them to module scope
1593
+ const STATIC = <em>hi</em>;`
1594
+ });
1246
1595
  } else if (c.kind === "function") {
1247
- 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}>.` });
1596
+ out.push({
1597
+ kind: "useCallback",
1598
+ owner,
1599
+ target: r.component,
1600
+ prop: root,
1601
+ label: `useCallback(${root}) in <${owner}>`,
1602
+ detail: `prop "${c.path}" of <${r.component}> is a new function on every render of <${owner}>.`,
1603
+ snippet: `// ${owner}
1604
+ import { useCallback } from 'react';
1605
+
1606
+ const ${id} = useCallback((/* args */) => {
1607
+ // ...
1608
+ }, [/* deps */]);
1609
+
1610
+ <${r.component} ${root}={${id}} />`
1611
+ });
1248
1612
  } else if (c.kind === "element") {
1249
- 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}>.` });
1613
+ out.push({
1614
+ kind: "useMemoElement",
1615
+ owner,
1616
+ target: r.component,
1617
+ prop: root,
1618
+ label: `memoize element prop ${root} in <${owner}>`,
1619
+ detail: `prop "${c.path}" of <${r.component}> is a new element with the same type and props on every render of <${owner}>.`,
1620
+ snippet: `// ${owner}
1621
+ import { useMemo } from 'react';
1622
+
1623
+ const ${id} = useMemo(() => ${shortValue(c.next, 40)}, [/* deps */]);
1624
+ // or pass it as children from a component that does not re-render`
1625
+ });
1250
1626
  } else {
1251
- 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}>.` });
1627
+ const isArray = Array.isArray(c.next);
1628
+ out.push({
1629
+ kind: "useMemo",
1630
+ owner,
1631
+ target: r.component,
1632
+ prop: root,
1633
+ label: `useMemo(${root}) in <${owner}>`,
1634
+ detail: `prop "${c.path}" of <${r.component}> is a new ${isArray ? "array" : "object"} with the same contents on every render of <${owner}>.`,
1635
+ snippet: `// ${owner}
1636
+ import { useMemo } from 'react';
1637
+
1638
+ const ${id} = useMemo(() => (${shortValue(c.next, 80)}), [/* deps */]);
1639
+
1640
+ // or, when it never changes, hoist it to module scope:
1641
+ const ${id.toUpperCase()} = ${shortValue(c.next, 80)};`
1642
+ });
1252
1643
  }
1253
1644
  }
1254
- for (const c of [...r.stateChanges, ...r.hookChanges]) {
1255
- const isContext = c.hook === "useContext";
1645
+ for (const c of [...r.stateChanges || [], ...r.hookChanges || []]) {
1646
+ const isContext = c.hook === "useContext" || /^useContext/.test(c.path);
1256
1647
  const ctxName = isContext ? (/useContext\((.*)\)/.exec(c.path) || [])[1] || "Context" : "";
1257
1648
  const providerOwner = isContext && c.provider && c.provider.component ? c.provider.component : null;
1258
1649
  if (isContext && c.kind === "different" && c.changedKeys && typeof c.totalKeys === "number" && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys) {
@@ -1262,20 +1653,63 @@ function fixesFor(r) {
1262
1653
  target: r.component,
1263
1654
  prop: ctxName,
1264
1655
  label: `split ${ctxName}${providerOwner ? ` in <${providerOwner}>` : ""}: only ${c.changedKeys.join(", ")} changed`,
1265
- detail: `${c.changedKeys.length} of ${c.totalKeys} keys changed in ${ctxName}, yet every consumer re-rendered.`
1656
+ 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.`,
1657
+ snippet: `// ${providerOwner || "Provider"}
1658
+ // one context per independently-changing slice
1659
+ const ${identifier(ctxName)}Static = createContext(...);
1660
+ const ${identifier(ctxName)}${c.changedKeys.map((k) => k[0].toUpperCase() + k.slice(1)).join("")} = createContext(...);
1661
+
1662
+ // or keep one context and let consumers select a slice:
1663
+ const ${c.changedKeys[0]} = useContextSelector(${ctxName}, (v) => v.${c.changedKeys[0]});`
1266
1664
  });
1267
1665
  continue;
1268
1666
  }
1269
- if (!AVOIDABLE.has(c.kind)) continue;
1667
+ if (!AVOIDABLE_KINDS.has(c.kind)) continue;
1270
1668
  if (isContext) {
1271
- 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.` });
1669
+ out.push({
1670
+ kind: "contextValue",
1671
+ owner: providerOwner || `${ctxName}.Provider`,
1672
+ target: r.component,
1673
+ prop: ctxName,
1674
+ label: `memoize the ${ctxName} provider value${providerOwner ? ` in <${providerOwner}>` : ""}`,
1675
+ detail: `<${r.component}> re-rendered because ${ctxName} produced a new value that is deep-equal to the previous one${providerOwner ? ` (Provider rendered by <${providerOwner}>)` : ""}.`,
1676
+ snippet: `// ${providerOwner || `where <${ctxName}.Provider> is rendered`}
1677
+ const value = useMemo(() => ({ /* ... */ }), [/* deps */]);
1678
+ <${ctxName}.Provider value={value}>`
1679
+ });
1272
1680
  } else if (c.hook === "useSyncExternalStore") {
1273
1681
  const chain = c.custom || [];
1274
1682
  const redux = chain.some((n) => /^use(App)?Selector$/.test(n));
1275
1683
  const zustand = !redux && chain.some((n) => /^use[A-Z]\w*Store$/.test(n) || n === "useStore" || n === "useBoundStore");
1276
- 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) });
1684
+ const via = chain.length ? ` via ${chain.join(" \u203A ")}` : "";
1685
+ out.push({
1686
+ kind: "storeSnapshot",
1687
+ owner: r.component,
1688
+ target: r.component,
1689
+ prop: c.path,
1690
+ label: redux ? `memoize the selector in <${r.component}>` : zustand ? `useShallow in <${r.component}>` : `stable getSnapshot in <${r.component}>`,
1691
+ 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.`,
1692
+ snippet: redux ? `// ${r.component}
1693
+ import { shallowEqual } from 'react-redux';
1694
+ const slice = useSelector(selectSlice, shallowEqual);
1695
+ // or memoize: const selectSlice = createSelector([selectA, selectB], (a, b) => ({ a, b }));` : zustand ? `// ${r.component}
1696
+ import { useShallow } from 'zustand/react/shallow';
1697
+ const { a, b } = useStore(useShallow((s) => ({ a: s.a, b: s.b })));
1698
+ // or select a primitive: const a = useStore((s) => s.a);` : `// ${r.component}
1699
+ // getSnapshot must return the same reference while the data is unchanged
1700
+ const snapshot = useSyncExternalStore(subscribe, store.getSnapshot /* cached */);`
1701
+ });
1277
1702
  } else {
1278
- 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).` });
1703
+ out.push({
1704
+ kind: "bailout",
1705
+ owner: r.component,
1706
+ target: r.component,
1707
+ prop: c.path,
1708
+ label: `bail out before setting ${c.path} in <${r.component}>`,
1709
+ detail: `${c.path} was set to a value deep-equal to the current one (new reference).`,
1710
+ snippet: `// ${r.component}
1711
+ setState((prev) => (deepEqual(prev, next) ? prev : next));`
1712
+ });
1279
1713
  }
1280
1714
  }
1281
1715
  return out;
@@ -1289,11 +1723,12 @@ function rankFixes(reports) {
1289
1723
  const key = fixKey(f);
1290
1724
  let agg = byKey.get(key);
1291
1725
  if (!agg) {
1292
- agg = { ...f, key, count: 0, components: {} };
1726
+ agg = { ...f, key, count: 0, components: {}, reports: [] };
1293
1727
  byKey.set(key, agg);
1294
1728
  }
1295
1729
  agg.count++;
1296
1730
  agg.components[r.component] = (agg.components[r.component] || 0) + 1;
1731
+ if (agg.reports.length < MAX_FIX_REPORTS) agg.reports.push(r);
1297
1732
  }
1298
1733
  }
1299
1734
  return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
@@ -1303,6 +1738,8 @@ function formatFixes(reports, limit = 10) {
1303
1738
  if (!ranked.length) return "No avoidable re-renders.";
1304
1739
  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(", ")})`);
1305
1740
  if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
1741
+ const roots = formatRootCauses(reports, 5);
1742
+ if (roots) lines.push("", "Root causes:", roots);
1306
1743
  return lines.join("\n");
1307
1744
  }
1308
1745
 
@@ -1359,6 +1796,8 @@ function compareSummaries(before, after) {
1359
1796
  regressions: rows.filter((r) => r.delta > 0)
1360
1797
  };
1361
1798
  }
1799
+ var summarizeSession = (session, reports) => summarizeReports(reports, session);
1800
+ var compareSessions = compareSummaries;
1362
1801
  function formatComparison(c) {
1363
1802
  const w = Math.max(14, ...c.rows.map((r) => r.component.length + 2));
1364
1803
  const line = (label, b, a, d) => `${label.padEnd(w)} ${String(b).padStart(8)} ${String(a).padStart(8)} ${d.padStart(8)}`;
@@ -1557,7 +1996,7 @@ function flash(target, duration = 500) {
1557
1996
  }
1558
1997
 
1559
1998
  // src/version.ts
1560
- var VERSION = "0.4.0" ;
1999
+ var VERSION = "0.7.0" ;
1561
2000
 
1562
2001
  // src/devtools.ts
1563
2002
  var DEVTOOLS_MARKER = "__rerenderLens";
@@ -1683,6 +2122,18 @@ function createDevtoolsNotifier(options = {}) {
1683
2122
  target.postMessage(msg, "*");
1684
2123
  };
1685
2124
  const source = options.source ?? "page";
2125
+ const trackingSummary = () => {
2126
+ const s = getState();
2127
+ const o = s.options;
2128
+ return {
2129
+ mode: o.trackAllComponents ? "all" : o.trackAllMemoized ? "memoized" : "marked",
2130
+ include: (o.include ?? []).map(matcherToString).filter((x) => x !== null),
2131
+ exclude: (o.exclude ?? []).map(matcherToString).filter((x) => x !== null),
2132
+ renderedCount: s.seen.size,
2133
+ trackedCount: s.seenTracked.size,
2134
+ overflow: s.seenOverflow
2135
+ };
2136
+ };
1686
2137
  const info = () => ({
1687
2138
  count: buffer.length,
1688
2139
  library: VERSION,
@@ -1696,7 +2147,8 @@ function createDevtoolsNotifier(options = {}) {
1696
2147
  scheduled: getState().scheduled,
1697
2148
  commits: getState().commits,
1698
2149
  overhead: { totalMs: getState().overheadMs, maxCommitMs: getState().maxCommitMs },
1699
- truncated: getState().truncated
2150
+ truncated: getState().truncated,
2151
+ tracking: trackingSummary()
1700
2152
  });
1701
2153
  const bridge = {
1702
2154
  replay: () => {
@@ -1742,6 +2194,10 @@ function createDevtoolsNotifier(options = {}) {
1742
2194
  flashAvoidable: (on) => {
1743
2195
  flashOn = !!on;
1744
2196
  },
2197
+ explain: (target2) => {
2198
+ const comp = typeof target2 === "number" ? fiberById(target2) : nearestComponent(fiberForNode(target2));
2199
+ return comp ? explainTracking(fiberType(comp), getState().options) : null;
2200
+ },
1745
2201
  inspect: (node) => {
1746
2202
  const comp = nearestComponent(fiberForNode(node));
1747
2203
  if (!comp) return null;
@@ -1756,6 +2212,7 @@ function createDevtoolsNotifier(options = {}) {
1756
2212
  component: fiberName(comp),
1757
2213
  instanceId: id,
1758
2214
  tracked: shouldTrack(fiberType(comp), getState().options),
2215
+ tracking: explainTracking(fiberType(comp), getState().options),
1759
2216
  path,
1760
2217
  reports: id === null ? [] : buffer.filter((e) => e.instanceId === id).slice(-20).map((e) => e.payload)
1761
2218
  };
@@ -1790,6 +2247,9 @@ function createDevtoolsNotifier(options = {}) {
1790
2247
  bridge.flashAvoidable(!!data.arg);
1791
2248
  reply.result = true;
1792
2249
  break;
2250
+ case "explain":
2251
+ reply.result = bridge.explain(data.arg);
2252
+ break;
1793
2253
  default:
1794
2254
  reply.error = `unknown command ${String(data.cmd)}`;
1795
2255
  }
@@ -1798,6 +2258,7 @@ function createDevtoolsNotifier(options = {}) {
1798
2258
  }
1799
2259
  return reply;
1800
2260
  };
2261
+ const isRelayGreeting = (data) => !!data && typeof data === "object" && data.__rerenderLensRelay === true && typeof data.app === "string";
1801
2262
  const isCommand = (data) => !!data && typeof data === "object" && data.__rerenderLensCmd === true && typeof data.id === "string";
1802
2263
  const envelope = (type, payload) => ({ [DEVTOOLS_MARKER]: true, version: PROTOCOL_VERSION, type, payload });
1803
2264
  const sinks = [];
@@ -1820,14 +2281,20 @@ function createDevtoolsNotifier(options = {}) {
1820
2281
  const relayUrl = options.relay ?? (typeof window !== "undefined" ? window.__RERENDER_LENS_RELAY__ : void 0);
1821
2282
  const ES = options.eventSource ?? (typeof EventSource === "function" ? EventSource : null);
1822
2283
  if (relayUrl && ES && typeof fetch === "function") {
1823
- const base = relayUrl.replace(/\/$/, "");
2284
+ const [rawBase, relayQuery = ""] = relayUrl.split("?");
2285
+ const base = (rawBase ?? "").replace(/\/$/, "");
2286
+ const relayToken = new URLSearchParams(relayQuery).get("token");
2287
+ const auth = relayToken ? `token=${encodeURIComponent(relayToken)}` : "";
1824
2288
  let queue = [];
1825
2289
  let scheduled = false;
2290
+ let appId = null;
1826
2291
  const flush = () => {
1827
2292
  scheduled = false;
1828
2293
  const batch = queue;
1829
2294
  queue = [];
1830
- fetch(`${base}/message`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(batch), keepalive: true }).catch(() => {
2295
+ const params = [appId ? `app=${encodeURIComponent(appId)}` : "", auth].filter(Boolean).join("&");
2296
+ const to = `${base}/message${params ? `?${params}` : ""}`;
2297
+ fetch(to, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(batch), keepalive: true }).catch(() => {
1831
2298
  });
1832
2299
  };
1833
2300
  const relaySend = (message) => {
@@ -1838,7 +2305,8 @@ function createDevtoolsNotifier(options = {}) {
1838
2305
  }
1839
2306
  };
1840
2307
  try {
1841
- const stream = new ES(`${base}/events?role=app`);
2308
+ const label = typeof location !== "undefined" ? `${location.host}${location.pathname}`.slice(0, 120) : "";
2309
+ const stream = new ES(`${base}/events?role=app${label ? `&label=${encodeURIComponent(label)}` : ""}${auth ? `&${auth}` : ""}`);
1842
2310
  stream.onmessage = (event) => {
1843
2311
  let parsed;
1844
2312
  try {
@@ -1846,7 +2314,10 @@ function createDevtoolsNotifier(options = {}) {
1846
2314
  } catch {
1847
2315
  return;
1848
2316
  }
1849
- for (const m of Array.isArray(parsed) ? parsed : [parsed]) if (isCommand(m)) relaySend(runCommand(m));
2317
+ for (const m of Array.isArray(parsed) ? parsed : [parsed]) {
2318
+ if (isRelayGreeting(m)) appId = m.app;
2319
+ else if (isCommand(m)) relaySend(runCommand(m));
2320
+ }
1850
2321
  };
1851
2322
  stream.onerror = () => {
1852
2323
  };
@@ -1889,47 +2360,64 @@ function createDevtoolsNotifier(options = {}) {
1889
2360
  };
1890
2361
  }
1891
2362
 
2363
+ exports.AVOIDABLE_KINDS = AVOIDABLE_KINDS;
1892
2364
  exports.DEFAULT_CHANNEL = DEFAULT_CHANNEL;
1893
2365
  exports.DEVTOOLS_MARKER = DEVTOOLS_MARKER;
2366
+ exports.KIND_LABEL = KIND_LABEL;
1894
2367
  exports.MARKER = MARKER;
1895
2368
  exports.PROTOCOL_VERSION = PROTOCOL_VERSION;
1896
2369
  exports.VERSION = VERSION;
2370
+ exports.analyzeCommit = analyzeCommit;
1897
2371
  exports.assertWithinBudget = assertWithinBudget;
1898
2372
  exports.avoidableCounts = avoidableCounts;
1899
2373
  exports.buildReport = buildReport;
2374
+ exports.cascadeTree = cascadeTree;
1900
2375
  exports.checkBudget = checkBudget;
1901
2376
  exports.classify = classify;
1902
2377
  exports.combineNotifiers = combineNotifiers;
2378
+ exports.compareSessions = compareSessions;
1903
2379
  exports.compareSummaries = compareSummaries;
1904
2380
  exports.configure = configure;
2381
+ exports.contextAttribution = contextAttribution;
1905
2382
  exports.createCollector = createCollector;
1906
2383
  exports.createDevtoolsNotifier = createDevtoolsNotifier;
1907
2384
  exports.customHooksFromStack = customHooksFromStack;
1908
2385
  exports.deepEqual = deepEqual;
1909
2386
  exports.deserializeOptions = deserializeOptions;
2387
+ exports.diffLeaves = diffLeaves;
1910
2388
  exports.diffRecords = diffRecords;
1911
2389
  exports.disable = disable;
1912
2390
  exports.ensureDevtoolsHook = ensureDevtoolsHook;
2391
+ exports.explainTracking = explainTracking;
2392
+ exports.firstDifferentPath = firstDifferentPath;
1913
2393
  exports.fixKey = fixKey;
1914
2394
  exports.fixesFor = fixesFor;
1915
2395
  exports.formatComparison = formatComparison;
1916
2396
  exports.formatFixes = formatFixes;
2397
+ exports.formatRootCauses = formatRootCauses;
1917
2398
  exports.getDisplayName = getDisplayName;
1918
2399
  exports.getRenderers = getRenderers;
2400
+ exports.groupByCommit = groupByCommit;
1919
2401
  exports.hookLabel = hookLabel;
2402
+ exports.indexByComponent = indexByComponent;
1920
2403
  exports.init = init;
1921
2404
  exports.isEnabled = isEnabled;
1922
2405
  exports.isProductionReact = isProductionReact;
1923
2406
  exports.parseExport = parseExport;
1924
2407
  exports.printReport = printReport;
1925
2408
  exports.rankFixes = rankFixes;
2409
+ exports.rankRootCauses = rankRootCauses;
1926
2410
  exports.resolveHookNames = resolveHookNames;
2411
+ exports.rootCauseOf = rootCauseOf;
2412
+ exports.rootCauseSummary = rootCauseSummary;
1927
2413
  exports.serialize = serialize;
1928
2414
  exports.serializeOptions = serializeOptions;
2415
+ exports.shortValue = shortValue;
1929
2416
  exports.shouldTrack = shouldTrack;
1930
2417
  exports.storeAdvice = storeAdvice;
1931
2418
  exports.summarize = summarize;
1932
2419
  exports.summarizeReports = summarizeReports;
2420
+ exports.summarizeSession = summarizeSession;
1933
2421
  exports.toBudget = toBudget;
1934
2422
  exports.track = track;
1935
2423
  exports.useWhyRerender = useWhyRerender;