rerender-lens 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +192 -362
  3. package/dist/{budget-COBu7jBU.d.cts → budget-BnJlYwrV.d.ts} +20 -10
  4. package/dist/{budget-LkNjGtRc.d.ts → budget-CRswn-nC.d.cts} +20 -10
  5. package/dist/cli.cjs +248 -31
  6. package/dist/cli.cjs.map +1 -1
  7. package/dist/cli.js +248 -31
  8. package/dist/cli.js.map +1 -1
  9. package/dist/{devtools-CZebzpF6.d.ts → devtools-BYWat7nQ.d.ts} +7 -1
  10. package/dist/{devtools-BkCct3cJ.d.cts → devtools-DJi_6AfJ.d.cts} +7 -1
  11. package/dist/index.cjs +439 -41
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.d.cts +131 -8
  14. package/dist/index.d.ts +131 -8
  15. package/dist/index.js +424 -42
  16. package/dist/index.js.map +1 -1
  17. package/dist/jest-setup.cjs +1673 -0
  18. package/dist/jest-setup.cjs.map +1 -0
  19. package/dist/jest-setup.d.cts +8 -0
  20. package/dist/jest-setup.d.ts +8 -0
  21. package/dist/jest-setup.js +1671 -0
  22. package/dist/jest-setup.js.map +1 -0
  23. package/dist/jest.cjs +1760 -0
  24. package/dist/jest.cjs.map +1 -0
  25. package/dist/jest.d.cts +23 -0
  26. package/dist/jest.d.ts +23 -0
  27. package/dist/jest.js +1751 -0
  28. package/dist/jest.js.map +1 -0
  29. package/dist/{notifiers-BjjGSHpp.d.ts → notifiers-DwKlvv4H.d.cts} +8 -4
  30. package/dist/{notifiers-BGQtWKfX.d.cts → notifiers-FNzKHs0a.d.ts} +8 -4
  31. package/dist/playwright.cjs +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 +10 -2
  38. package/dist/relay.cjs.map +1 -1
  39. package/dist/relay.js +10 -2
  40. package/dist/relay.js.map +1 -1
  41. package/dist/rerender-lens.iife.js +356 -54
  42. package/dist/runner-report-B8Sc3FZO.d.cts +46 -0
  43. package/dist/runner-report-CW8X5RwK.d.ts +46 -0
  44. package/dist/setup.cjs +112 -27
  45. package/dist/setup.cjs.map +1 -1
  46. package/dist/setup.js +112 -27
  47. package/dist/setup.js.map +1 -1
  48. package/dist/{types-BzUEVkxJ.d.cts → types-DW5-N2XH.d.cts} +55 -2
  49. package/dist/{types-BzUEVkxJ.d.ts → types-DW5-N2XH.d.ts} +55 -2
  50. package/dist/vite.d.cts +1 -1
  51. package/dist/vite.d.ts +1 -1
  52. package/dist/vitest-setup.cjs +337 -41
  53. package/dist/vitest-setup.cjs.map +1 -1
  54. package/dist/vitest-setup.d.cts +4 -4
  55. package/dist/vitest-setup.d.ts +4 -4
  56. package/dist/vitest-setup.js +337 -41
  57. package/dist/vitest-setup.js.map +1 -1
  58. package/dist/vitest.cjs +400 -77
  59. package/dist/vitest.cjs.map +1 -1
  60. package/dist/vitest.d.cts +21 -39
  61. package/dist/vitest.d.ts +21 -39
  62. package/dist/vitest.js +401 -78
  63. package/dist/vitest.js.map +1 -1
  64. package/package.json +40 -4
  65. package/panel/panel.css +1 -0
  66. package/panel/panel.js +499 -309
@@ -1,4 +1,4 @@
1
- /* rerender-lens 0.4.0 — rerender-lens.core.js: types, version, diff, report, state, overlay. Built by scripts/build-vendor.mjs; do not edit. */
1
+ /* rerender-lens 0.5.0 — rerender-lens.core.js: types, version, diff, report, state, overlay. Built by scripts/build-vendor.mjs; do not edit. */
2
2
  (() => {
3
3
  var __defProp = Object.defineProperty;
4
4
  var __export = (target, all) => {
@@ -18,16 +18,18 @@
18
18
  __export(version_exports, {
19
19
  VERSION: () => VERSION
20
20
  });
21
- var VERSION = true ? "0.4.0" : "dev";
21
+ var VERSION = true ? "0.5.0" : "dev";
22
22
 
23
23
  // src/diff.ts
24
24
  var diff_exports = {};
25
25
  __export(diff_exports, {
26
+ AVOIDABLE_KINDS: () => AVOIDABLE_KINDS,
26
27
  DEFAULT_DIFF_BUDGET: () => DEFAULT_DIFF_BUDGET,
27
28
  beginDiffScope: () => beginDiffScope,
28
29
  classify: () => classify,
29
30
  deepEqual: () => deepEqual,
30
31
  diffBudgetExhausted: () => diffBudgetExhausted,
32
+ diffLeaves: () => diffLeaves,
31
33
  diffRecords: () => diffRecords,
32
34
  firstDifferentPath: () => firstDifferentPath,
33
35
  isReactElement: () => isReactElement
@@ -153,6 +155,7 @@
153
155
  }
154
156
  return false;
155
157
  }
158
+ var AVOIDABLE_KINDS = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
156
159
  function classify(prev, next) {
157
160
  if (typeof prev === "function" && typeof next === "function") {
158
161
  return prev.name === next.name && prev.toString() === next.toString() ? "function" : "different";
@@ -189,35 +192,53 @@
189
192
  if (Object.is(a, b)) continue;
190
193
  const kind = classify(a, b);
191
194
  if (kind === "different") {
192
- changes.push({ path: diffBudgetExhausted() ? path : firstDifferentPath(a, b, path), kind, prev: a, next: b });
195
+ changes.push({ path: diffBudgetExhausted() ? path : firstDifferentPath(a, b, path) ?? path, kind, prev: a, next: b });
193
196
  } else {
194
197
  changes.push({ path, kind, prev: a, next: b });
195
198
  }
196
199
  }
197
200
  return changes;
198
201
  }
199
- function firstDifferentPath(a, b, path, depth = 0) {
200
- if (depth > 8) return path;
202
+ function firstDifferentPath(a, b, path = "", depth = 0) {
203
+ if (depth > 8) return path || "(value)";
201
204
  if (Array.isArray(a) && Array.isArray(b)) {
202
- if (a.length !== b.length) return `${path}.length`;
205
+ if (a.length !== b.length) return path ? `${path}.length` : "length";
203
206
  for (let i = 0; i < a.length; i++) {
204
207
  if (!deepEqual(a[i], b[i])) return firstDifferentPath(a[i], b[i], joinPath(path, i), depth + 1);
205
208
  }
206
- return path;
209
+ return null;
207
210
  }
208
211
  if (isPlainObject(a) && isPlainObject(b)) {
209
212
  const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
210
213
  for (const k of keys) {
211
214
  if (!deepEqual(a[k], b[k])) return firstDifferentPath(a[k], b[k], joinPath(path, k), depth + 1);
212
215
  }
213
- return path;
216
+ return null;
214
217
  }
215
- return path;
218
+ return deepEqual(a, b) ? null : path || "(value)";
219
+ }
220
+ function diffLeaves(a, b, limit = 20, path = "", out = []) {
221
+ if (out.length >= limit || Object.is(a, b)) return out;
222
+ if (Array.isArray(a) && Array.isArray(b)) {
223
+ const n = Math.max(a.length, b.length);
224
+ for (let i = 0; i < n && out.length < limit; i++) diffLeaves(a[i], b[i], limit, joinPath(path, i), out);
225
+ return out;
226
+ }
227
+ if (isPlainObject(a) && isPlainObject(b)) {
228
+ for (const k of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) {
229
+ if (out.length >= limit) break;
230
+ diffLeaves(a[k], b[k], limit, joinPath(path, k), out);
231
+ }
232
+ return out;
233
+ }
234
+ if (!deepEqual(a, b)) out.push({ path: path || "(value)", prev: a, next: b });
235
+ return out;
216
236
  }
217
237
 
218
238
  // src/report.ts
219
239
  var report_exports = {};
220
240
  __export(report_exports, {
241
+ KIND_LABEL: () => KIND_LABEL,
221
242
  buildReport: () => buildReport,
222
243
  fixFor: () => fixFor,
223
244
  hookLabel: () => hookLabel,
@@ -239,10 +260,12 @@
239
260
  }
240
261
  var isGenuine = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
241
262
  var isChildren = (change) => change.path === "children";
263
+ var isRefObject = (change) => change.path === "ref" && change.kind === "deep-equal";
242
264
  function fixFor(change) {
243
265
  if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
244
266
  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`;
245
267
  }
268
+ if (isRefObject(change)) return `create the ref once with useRef (or createRef outside the render) and pass the same object every time`;
246
269
  switch (change.kind) {
247
270
  case "function":
248
271
  return `wrap it in useCallback (or hoist it out of the parent's render)`;
@@ -258,6 +281,7 @@
258
281
  if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
259
282
  return `children are new React elements with the same types and props on every render of the parent`;
260
283
  }
284
+ if (isRefObject(change)) return `prop "ref" is a new ref object on every render (React re-attaches it each time)`;
261
285
  switch (change.kind) {
262
286
  case "deep-equal":
263
287
  return `prop "${change.path}" is a new reference but deep-equal to the previous value`;
@@ -285,11 +309,21 @@
285
309
  if (causes.length === 0) trigger = "parent";
286
310
  else if (causes.length === 1) trigger = causes[0];
287
311
  else trigger = "mixed";
288
- const avoidable = trigger === "parent";
312
+ const nothingChanged = input.propChanges.length === 0 && stateChanges.length === 0 && hookChanges.length === 0;
313
+ const cacheHit = input.compiled === true && trigger === "parent" && input.propChanges.length === 0;
314
+ const revealed = input.revealed === true && trigger === "parent";
315
+ const avoidable = trigger === "parent" && !cacheHit && !revealed;
289
316
  const reasons = [];
290
- if (avoidable && input.propChanges.length === 0 && stateChanges.length === 0 && hookChanges.length === 0) {
317
+ const memoFix = input.classComponent ? `Make "${input.component}" extend PureComponent (or implement shouldComponentUpdate)` : `Wrap "${input.component}" in React.memo (or extend PureComponent)`;
318
+ if (cacheHit) {
319
+ reasons.push(`compiled by React Compiler: its output is memoized and the inputs did not change, so this render is cheap; nothing to fix.`);
320
+ } else if (revealed) {
321
+ reasons.push(
322
+ `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.`
323
+ );
324
+ } else if (avoidable && nothingChanged) {
291
325
  const who = input.parent ? `<${input.parent.name}> re-rendered (${describeTrigger(input.parent.trigger)})` : "its parent re-rendered";
292
- reasons.push(`re-rendered with identical props because ${who}. Wrap "${input.component}" in React.memo (or extend PureComponent).`);
326
+ reasons.push(`re-rendered with identical props because ${who}. ${memoFix}.`);
293
327
  } else if (avoidable && input.parent) {
294
328
  reasons.push(`caused by <${input.parent.name}> re-rendering (${describeTrigger(input.parent.trigger)}).`);
295
329
  }
@@ -298,9 +332,9 @@
298
332
  reasons.push(fix ? `${describe(c)}: ${fix}.` : `${describe(c)}.`);
299
333
  }
300
334
  const memoized = input.memoized !== false;
301
- if (avoidable && !memoized && input.propChanges.length > 0) {
335
+ if (avoidable && !memoized && !input.compiled && input.propChanges.length > 0) {
302
336
  reasons.push(
303
- `"${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).`
337
+ `"${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)"}.`
304
338
  );
305
339
  }
306
340
  for (const c of stateChanges) {
@@ -312,6 +346,8 @@
312
346
  const where = c.provider && c.provider.component ? ` (provided by <${c.provider.component}>)` : "";
313
347
  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` : "";
314
348
  reasons.push(`${c.path} changed${where}${keys}.`);
349
+ } else if (c.hook === "useDeferredValue" && isGenuine(c)) {
350
+ 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.`);
315
351
  } else if (isGenuine(c)) reasons.push(`${hookLabel(c)} changed.`);
316
352
  else if (isStateHook(c))
317
353
  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.`);
@@ -342,6 +378,7 @@
342
378
  reasons,
343
379
  time: now()
344
380
  };
381
+ if (input.compiled) report.compiled = true;
345
382
  if (input.selfDuration !== void 0) report.selfDuration = input.selfDuration;
346
383
  if (input.treeDuration !== void 0) report.treeDuration = input.treeDuration;
347
384
  if (input.commitPriority) report.commitPriority = input.commitPriority;
@@ -379,10 +416,10 @@
379
416
  };
380
417
  function summarize(report) {
381
418
  const counts = /* @__PURE__ */ new Map();
382
- for (const c of [...report.propChanges, ...report.stateChanges, ...report.hookChanges]) {
419
+ for (const c of [...report.propChanges || [], ...report.stateChanges || [], ...report.hookChanges || []]) {
383
420
  counts.set(c.kind, (counts.get(c.kind) ?? 0) + 1);
384
421
  }
385
- const parts2 = [...counts].map(([kind, n]) => `${n} ${KIND_LABEL[kind]}`);
422
+ const parts2 = [...counts].map(([kind, n]) => `${n} ${KIND_LABEL[kind] || kind}`);
386
423
  if (parts2.length === 0) parts2.push("no changes");
387
424
  return parts2.join(", ");
388
425
  }
@@ -575,7 +612,7 @@
575
612
  })();
576
613
 
577
614
  ;
578
- /* rerender-lens 0.4.0 — rerender-lens.engine.js: fiber, tracker, hookNames. Built by scripts/build-vendor.mjs; do not edit. */
615
+ /* rerender-lens 0.5.0 — rerender-lens.engine.js: fiber, tracker, hookNames. Built by scripts/build-vendor.mjs; do not edit. */
579
616
  (() => {
580
617
  var __create = Object.create;
581
618
  var __defProp = Object.defineProperty;
@@ -660,6 +697,7 @@
660
697
  getRenderers: () => getRenderers,
661
698
  hostNodesOf: () => hostNodesOf,
662
699
  instanceIdOf: () => instanceIdOf,
700
+ isCompiledFiber: () => isCompiledFiber,
663
701
  isMemoizedFiber: () => isMemoizedFiber,
664
702
  isProductionReact: () => isProductionReact,
665
703
  nearestComponent: () => nearestComponent,
@@ -785,6 +823,7 @@
785
823
  useEffectEvent: 1,
786
824
  useCacheRefresh: 1
787
825
  };
826
+ var BUILTIN_HOOKS = /* @__PURE__ */ new Set([...Object.keys(NODE_COUNT), "useContext", "useDebugValue", "use"]);
788
827
  var noop = () => {
789
828
  };
790
829
  function customHooksFromStack(stack, componentName, marker) {
@@ -799,7 +838,7 @@
799
838
  continue;
800
839
  }
801
840
  if (name === componentName || name.endsWith("." + componentName)) break;
802
- if (/^use[A-Z0-9_]/.test(name)) names.push(name);
841
+ if (/^use[A-Z0-9_]/.test(name) && !BUILTIN_HOOKS.has(name)) names.push(name);
803
842
  if (names.length > 8) break;
804
843
  }
805
844
  return names;
@@ -1101,6 +1140,11 @@
1101
1140
  }
1102
1141
  return false;
1103
1142
  }
1143
+ function isCompiledFiber(fiber) {
1144
+ if (fiber.tag === ClassComponent) return false;
1145
+ const cache2 = fiber.updateQueue?.memoCache;
1146
+ return !!cache2 && Array.isArray(cache2.data);
1147
+ }
1104
1148
  function durationsOf(fiber) {
1105
1149
  if (typeof fiber.actualDuration !== "number") return null;
1106
1150
  const tree = fiber.actualDuration;
@@ -1279,7 +1323,8 @@
1279
1323
  const labels = hookLabelsFor(fiber, b);
1280
1324
  let i = 0;
1281
1325
  while (a && b) {
1282
- if ((isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
1326
+ const deferred = labels?.[i] === "useDeferredValue";
1327
+ if ((deferred || isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
1283
1328
  const hook = labels?.[i] ?? hookLabel(b);
1284
1329
  out.push({
1285
1330
  path: `${hook}#${i}`,
@@ -1313,6 +1358,10 @@
1313
1358
  let a = alt.dependencies?.firstContext ?? null;
1314
1359
  let b = fiber.dependencies?.firstContext ?? null;
1315
1360
  let i = 0;
1361
+ if (b && !("memoizedValue" in b)) {
1362
+ (0, import_state2.warnOnce)("contextValues", "this React does not record context values on the fiber (React < 18): context changes are reported as plain parent re-renders.");
1363
+ return out;
1364
+ }
1316
1365
  while (a && b) {
1317
1366
  if (a.context === b.context && !Object.is(a.memoizedValue, b.memoizedValue)) {
1318
1367
  const name = b.context.displayName ?? "Context";
@@ -1345,10 +1394,28 @@
1345
1394
  return out;
1346
1395
  }
1347
1396
  var isGenuine = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
1397
+ var isRefObject = (v) => {
1398
+ if (typeof v !== "object" || v === null) return false;
1399
+ const keys = Object.keys(v);
1400
+ return keys.length === 1 && keys[0] === "current";
1401
+ };
1402
+ function diffProps(prev, next) {
1403
+ if (prev === next) return [];
1404
+ const pr = prev.ref;
1405
+ const nr = next.ref;
1406
+ if (pr !== nr && isRefObject(pr) && isRefObject(nr)) {
1407
+ const { ref: _p, ...restPrev } = prev;
1408
+ const { ref: _n, ...restNext } = next;
1409
+ void _p;
1410
+ void _n;
1411
+ return [{ path: "ref", kind: "deep-equal", prev: pr, next: nr }, ...(0, import_diff.diffRecords)(restPrev, restNext)];
1412
+ }
1413
+ return (0, import_diff.diffRecords)(prev, next);
1414
+ }
1348
1415
  function analyze(fiber, alt, trackHooks) {
1349
1416
  const prevProps = alt.memoizedProps ?? {};
1350
1417
  const nextProps = fiber.memoizedProps ?? {};
1351
- const propChanges = prevProps === nextProps ? [] : (0, import_diff.diffRecords)(prevProps, nextProps);
1418
+ const propChanges = diffProps(prevProps, nextProps);
1352
1419
  let stateChanges = [];
1353
1420
  let hookChanges = [];
1354
1421
  if (fiber.tag === ClassComponent) {
@@ -1361,7 +1428,7 @@
1361
1428
  } else if (trackHooks) {
1362
1429
  hookChanges = [...diffHooks(fiber, alt), ...diffContexts(fiber, alt)];
1363
1430
  }
1364
- const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore";
1431
+ const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore" && h.hook !== "useDeferredValue";
1365
1432
  const causes = [];
1366
1433
  if (propChanges.some(isGenuine)) causes.push("props");
1367
1434
  if (stateChanges.some(isGenuine) || hookChanges.some((h) => isStateHook(h) && isGenuine(h))) causes.push("state");
@@ -1401,6 +1468,24 @@
1401
1468
  var EFFECT_LOOP_WINDOW_MS = 50;
1402
1469
  var MAX_REPORTS_PER_COMMIT = 200;
1403
1470
  var COMMIT_TIME_BUDGET_MS = 25;
1471
+ function isDeferredCatchUp(fiber) {
1472
+ const types = fiber._debugHookTypes;
1473
+ if (!types || !types.includes("useDeferredValue") || !fiber.alternate) return false;
1474
+ const first = fiber.memoizedState;
1475
+ if (!isHookList(first)) return false;
1476
+ const labels = hookLabelsFor(fiber, first);
1477
+ if (!labels) return false;
1478
+ let a = fiber.alternate.memoizedState;
1479
+ let b = first;
1480
+ for (let i = 0; a && b; a = a.next, b = b.next, i++) {
1481
+ if (labels[i] === "useDeferredValue" && !Object.is(a.memoizedState, b.memoizedState)) return true;
1482
+ }
1483
+ return false;
1484
+ }
1485
+ function isUnder(fiber, boundaries) {
1486
+ for (let f = fiber.return; f && f.tag !== HostRoot; f = f.return) if (f.tag === SuspenseComponent && boundaries.includes(f)) return true;
1487
+ return false;
1488
+ }
1404
1489
  function onCommit(root, commitPriority) {
1405
1490
  const s = (0, import_state2.getState)();
1406
1491
  if (!s.enabled) return;
@@ -1410,7 +1495,7 @@
1410
1495
  const rendered = [];
1411
1496
  const renderedNames = /* @__PURE__ */ new Set();
1412
1497
  let hot = false;
1413
- let suspenseResolved = false;
1498
+ let resolvedBoundaries = null;
1414
1499
  const stack = [root.current];
1415
1500
  while (stack.length) {
1416
1501
  const fiber = stack.pop();
@@ -1420,7 +1505,7 @@
1420
1505
  renderedNames.add(fiberName(fiber));
1421
1506
  if (shouldTrack(fiberType(fiber), o)) rendered.push(fiber);
1422
1507
  }
1423
- if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) suspenseResolved = true;
1508
+ if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) (resolvedBoundaries ?? (resolvedBoundaries = [])).push(fiber);
1424
1509
  if (!alt || fiber.child !== alt.child) {
1425
1510
  for (let c = fiber.child; c; c = c.sibling) stack.push(c);
1426
1511
  }
@@ -1432,10 +1517,10 @@
1432
1517
  let commitCause;
1433
1518
  let afterCommit;
1434
1519
  const inputDriven = commitPriority === "immediate" || commitPriority === "user-blocking";
1435
- if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u))) {
1520
+ if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u)) && !rendered.some(isDeferredCatchUp)) {
1436
1521
  commitCause = "effect-after-commit";
1437
1522
  afterCommit = lastCommit.id;
1438
- } else if (suspenseResolved) commitCause = "suspense-resolved";
1523
+ } else if (resolvedBoundaries) commitCause = "suspense-resolved";
1439
1524
  if (rendered.length === 0) {
1440
1525
  lastCommit = { id: s.nextCommitId, at, rendered: renderedNames };
1441
1526
  return;
@@ -1487,6 +1572,9 @@
1487
1572
  owner: ownerName(fiber),
1488
1573
  path: componentPath(fiber),
1489
1574
  memoized: isMemoizedFiber(fiber),
1575
+ compiled: isCompiledFiber(fiber),
1576
+ classComponent: fiber.tag === ClassComponent,
1577
+ revealed: resolvedBoundaries !== null && a.trigger === "parent" && isUnder(fiber, resolvedBoundaries),
1490
1578
  selfDuration: durations ? durations.self : void 0,
1491
1579
  treeDuration: durations ? durations.tree : void 0,
1492
1580
  commitId,
@@ -1508,7 +1596,7 @@
1508
1596
  })();
1509
1597
 
1510
1598
  ;
1511
- /* rerender-lens 0.4.0 — rerender-lens.js: devtools bridge + notifiers (window.RerenderLens). Built by scripts/build-vendor.mjs; do not edit. */
1599
+ /* rerender-lens 0.5.0 — rerender-lens.js: devtools bridge + notifiers (window.RerenderLens). Built by scripts/build-vendor.mjs; do not edit. */
1512
1600
  "use strict";
1513
1601
  var RerenderLens = (() => {
1514
1602
  var __create = Object.create;
@@ -1621,14 +1709,14 @@ var RerenderLens = (() => {
1621
1709
  MARKER: () => import_types.MARKER,
1622
1710
  PROTOCOL_VERSION: () => PROTOCOL_VERSION,
1623
1711
  VERSION: () => import_version2.VERSION,
1624
- buildReport: () => import_report2.buildReport,
1625
- classify: () => import_diff2.classify,
1712
+ buildReport: () => import_report.buildReport,
1713
+ classify: () => import_diff4.classify,
1626
1714
  combineNotifiers: () => combineNotifiers,
1627
1715
  configure: () => import_tracker2.configure,
1628
1716
  createCollector: () => createCollector,
1629
1717
  createDevtoolsNotifier: () => createDevtoolsNotifier,
1630
- deepEqual: () => import_diff2.deepEqual,
1631
- diffRecords: () => import_diff2.diffRecords,
1718
+ deepEqual: () => import_diff4.deepEqual,
1719
+ diffRecords: () => import_diff4.diffRecords,
1632
1720
  disable: () => import_tracker2.disable,
1633
1721
  ensureDevtoolsHook: () => import_fiber2.ensureDevtoolsHook,
1634
1722
  getDisplayName: () => import_tracker2.getDisplayName,
@@ -1636,51 +1724,219 @@ var RerenderLens = (() => {
1636
1724
  init: () => import_tracker2.init,
1637
1725
  isEnabled: () => import_tracker2.isEnabled,
1638
1726
  isProductionReact: () => import_fiber2.isProductionReact,
1639
- printReport: () => import_report2.printReport,
1727
+ printReport: () => import_report.printReport,
1640
1728
  serialize: () => serialize,
1641
1729
  shouldTrack: () => import_tracker2.shouldTrack,
1642
- summarize: () => import_report2.summarize,
1730
+ summarize: () => import_report.summarize,
1643
1731
  track: () => import_tracker2.track
1644
1732
  });
1645
1733
  var import_types = __toESM(require_types(), 1);
1646
1734
  var import_tracker2 = __toESM(require_tracker(), 1);
1647
1735
  var import_fiber2 = __toESM(require_fiber(), 1);
1736
+ var import_diff4 = __toESM(require_diff(), 1);
1737
+ var import_report = __toESM(require_report(), 1);
1738
+
1739
+ // src/fixes.ts
1648
1740
  var import_diff2 = __toESM(require_diff(), 1);
1649
- var import_report2 = __toESM(require_report(), 1);
1741
+
1742
+ // src/causes.ts
1743
+ var import_diff = __toESM(require_diff(), 1);
1744
+ 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);
1745
+ var bump = (m, key, n = 1) => void m.set(key, (m.get(key) || 0) + n);
1746
+ function indexByComponent(reports) {
1747
+ const index = /* @__PURE__ */ new Map();
1748
+ for (const r of reports) {
1749
+ const list = index.get(r.component);
1750
+ if (list) list.push(r);
1751
+ else index.set(r.component, [r]);
1752
+ }
1753
+ return index;
1754
+ }
1755
+ function rootCauseOf(r, commitReports, index = indexByComponent(commitReports)) {
1756
+ let cur = r;
1757
+ const seen = /* @__PURE__ */ new Set([r]);
1758
+ while (cur.trigger === "parent" && cur.parent) {
1759
+ const parent = cur.parent;
1760
+ const candidates = index.get(parent.name);
1761
+ const p = candidates && candidates.find((x) => isAncestorReport(x, cur));
1762
+ if (!p || seen.has(p)) return { name: parent.name, trigger: parent.trigger, report: null };
1763
+ seen.add(p);
1764
+ cur = p;
1765
+ }
1766
+ return cur === r ? null : { name: cur.component, trigger: cur.trigger, report: cur };
1767
+ }
1768
+ function rootCausesOf(reports) {
1769
+ const roots = /* @__PURE__ */ new Map();
1770
+ const rootByReport = /* @__PURE__ */ new Map();
1771
+ const index = indexByComponent(reports);
1772
+ let avoidable = 0;
1773
+ let wasted = 0;
1774
+ for (const r of reports) {
1775
+ if (!r.avoidable) continue;
1776
+ avoidable++;
1777
+ if (typeof r.selfDuration === "number") wasted += r.selfDuration;
1778
+ const root = rootCauseOf(r, reports, index);
1779
+ const name = root ? root.name : r.parent && r.parent.name || "(unknown)";
1780
+ const trigger = root ? root.trigger : r.parent && r.parent.trigger || "parent";
1781
+ rootByReport.set(r, name);
1782
+ let agg = roots.get(name);
1783
+ if (!agg) {
1784
+ agg = { name, trigger, count: 0, components: /* @__PURE__ */ new Map() };
1785
+ roots.set(name, agg);
1786
+ }
1787
+ agg.count++;
1788
+ bump(agg.components, r.component);
1789
+ }
1790
+ return { roots: [...roots.values()].sort((a, b) => b.count - a.count), rootByReport, avoidable, wasted };
1791
+ }
1792
+ function groupByCommit(reports) {
1793
+ const out = /* @__PURE__ */ new Map();
1794
+ for (const r of reports) {
1795
+ if (typeof r.commitId !== "number" || r.commitId <= 0) continue;
1796
+ const list = out.get(r.commitId);
1797
+ if (list) list.push(r);
1798
+ else out.set(r.commitId, [r]);
1799
+ }
1800
+ return out;
1801
+ }
1802
+ function rankRootCauses(reports) {
1803
+ const byName = /* @__PURE__ */ new Map();
1804
+ for (const commit of groupByCommit(reports).values()) {
1805
+ for (const root of rootCausesOf(commit).roots) {
1806
+ let agg = byName.get(root.name);
1807
+ if (!agg) {
1808
+ agg = { name: root.name, trigger: root.trigger, commits: 0, count: 0, components: /* @__PURE__ */ new Map() };
1809
+ byName.set(root.name, agg);
1810
+ }
1811
+ agg.commits++;
1812
+ agg.count += root.count;
1813
+ for (const [c, n] of root.components) bump(agg.components, c, n);
1814
+ }
1815
+ }
1816
+ return [...byName.values()].sort((a, b) => b.count - a.count || b.commits - a.commits || a.name.localeCompare(b.name));
1817
+ }
1818
+ var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
1819
+ function formatRootCauses(reports, limit = 5) {
1820
+ const ranked = rankRootCauses(reports);
1821
+ 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(", ")})`);
1822
+ if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
1823
+ return lines.join("\n");
1824
+ }
1650
1825
 
1651
1826
  // src/fixes.ts
1652
- var import_report = __toESM(require_report(), 1);
1653
- var AVOIDABLE = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
1827
+ var MAX_FIX_REPORTS = 50;
1654
1828
  var rootOf = (path) => path.split(/[.[]/)[0] || path;
1829
+ var identifier = (name) => /^[A-Za-z_$][\w$]*$/.test(name) ? name : "value";
1830
+ function shortValue(v, max = 60) {
1831
+ let s;
1832
+ try {
1833
+ s = JSON.stringify(v);
1834
+ } catch {
1835
+ s = String(v);
1836
+ }
1837
+ if (s === void 0) s = String(v);
1838
+ return s.length > max ? s.slice(0, max - 3) + "..." : s;
1839
+ }
1655
1840
  function fixesFor(r) {
1656
1841
  const out = [];
1657
- const owner = r.owner || r.parent && r.parent.name || "?";
1658
- const changes = [...r.propChanges, ...r.stateChanges, ...r.hookChanges];
1842
+ const ownerName = r.owner || r.parent && r.parent.name || null;
1843
+ const propChanges = r.propChanges || [];
1844
+ const changes = [...propChanges, ...r.stateChanges || [], ...r.hookChanges || []];
1659
1845
  if (r.avoidable && (changes.length === 0 || r.memoized === false)) {
1846
+ const identical = changes.length === 0;
1660
1847
  out.push({
1661
1848
  kind: "memo",
1662
1849
  owner: r.component,
1663
1850
  target: r.component,
1664
1851
  prop: null,
1665
1852
  label: `Wrap <${r.component}> in React.memo`,
1666
- 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.`
1853
+ 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.`,
1854
+ snippet: `// ${r.component}
1855
+ import { memo } from 'react';
1856
+
1857
+ export const ${r.component} = memo(function ${r.component}(props) {
1858
+ // ...
1859
+ });
1860
+ // class components: extend PureComponent instead`
1667
1861
  });
1668
1862
  }
1669
- for (const c of r.propChanges) {
1670
- if (!AVOIDABLE.has(c.kind)) continue;
1863
+ for (const c of propChanges) {
1864
+ if (!import_diff2.AVOIDABLE_KINDS.has(c.kind)) continue;
1865
+ const owner = ownerName || "?";
1671
1866
  const root = rootOf(c.path);
1867
+ const id = identifier(root);
1672
1868
  if (root === "children" && (c.kind === "element" || c.kind === "deep-equal")) {
1673
- 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.` });
1869
+ out.push({
1870
+ kind: "children",
1871
+ owner,
1872
+ target: r.component,
1873
+ prop: "children",
1874
+ label: `memoize children of <${r.component}> in <${owner}>`,
1875
+ detail: `<${owner}> re-creates the children of <${r.component}> on every render; they have the same types and props each time.`,
1876
+ snippet: `// ${owner}
1877
+ import { useMemo } from 'react';
1878
+
1879
+ const children = useMemo(() => (
1880
+ <>{/* the same elements */}</>
1881
+ ), [/* deps */]);
1882
+
1883
+ <${r.component}>{children}</${r.component}>
1884
+
1885
+ // static children: hoist them to module scope
1886
+ const STATIC = <em>hi</em>;`
1887
+ });
1674
1888
  } else if (c.kind === "function") {
1675
- 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}>.` });
1889
+ out.push({
1890
+ kind: "useCallback",
1891
+ owner,
1892
+ target: r.component,
1893
+ prop: root,
1894
+ label: `useCallback(${root}) in <${owner}>`,
1895
+ detail: `prop "${c.path}" of <${r.component}> is a new function on every render of <${owner}>.`,
1896
+ snippet: `// ${owner}
1897
+ import { useCallback } from 'react';
1898
+
1899
+ const ${id} = useCallback((/* args */) => {
1900
+ // ...
1901
+ }, [/* deps */]);
1902
+
1903
+ <${r.component} ${root}={${id}} />`
1904
+ });
1676
1905
  } else if (c.kind === "element") {
1677
- 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}>.` });
1906
+ out.push({
1907
+ kind: "useMemoElement",
1908
+ owner,
1909
+ target: r.component,
1910
+ prop: root,
1911
+ label: `memoize element prop ${root} in <${owner}>`,
1912
+ detail: `prop "${c.path}" of <${r.component}> is a new element with the same type and props on every render of <${owner}>.`,
1913
+ snippet: `// ${owner}
1914
+ import { useMemo } from 'react';
1915
+
1916
+ const ${id} = useMemo(() => ${shortValue(c.next, 40)}, [/* deps */]);
1917
+ // or pass it as children from a component that does not re-render`
1918
+ });
1678
1919
  } else {
1679
- 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}>.` });
1920
+ const isArray = Array.isArray(c.next);
1921
+ out.push({
1922
+ kind: "useMemo",
1923
+ owner,
1924
+ target: r.component,
1925
+ prop: root,
1926
+ label: `useMemo(${root}) in <${owner}>`,
1927
+ detail: `prop "${c.path}" of <${r.component}> is a new ${isArray ? "array" : "object"} with the same contents on every render of <${owner}>.`,
1928
+ snippet: `// ${owner}
1929
+ import { useMemo } from 'react';
1930
+
1931
+ const ${id} = useMemo(() => (${shortValue(c.next, 80)}), [/* deps */]);
1932
+
1933
+ // or, when it never changes, hoist it to module scope:
1934
+ const ${id.toUpperCase()} = ${shortValue(c.next, 80)};`
1935
+ });
1680
1936
  }
1681
1937
  }
1682
- for (const c of [...r.stateChanges, ...r.hookChanges]) {
1683
- const isContext = c.hook === "useContext";
1938
+ for (const c of [...r.stateChanges || [], ...r.hookChanges || []]) {
1939
+ const isContext = c.hook === "useContext" || /^useContext/.test(c.path);
1684
1940
  const ctxName = isContext ? (/useContext\((.*)\)/.exec(c.path) || [])[1] || "Context" : "";
1685
1941
  const providerOwner = isContext && c.provider && c.provider.component ? c.provider.component : null;
1686
1942
  if (isContext && c.kind === "different" && c.changedKeys && typeof c.totalKeys === "number" && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys) {
@@ -1690,20 +1946,63 @@ var RerenderLens = (() => {
1690
1946
  target: r.component,
1691
1947
  prop: ctxName,
1692
1948
  label: `split ${ctxName}${providerOwner ? ` in <${providerOwner}>` : ""}: only ${c.changedKeys.join(", ")} changed`,
1693
- detail: `${c.changedKeys.length} of ${c.totalKeys} keys changed in ${ctxName}, yet every consumer re-rendered.`
1949
+ 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.`,
1950
+ snippet: `// ${providerOwner || "Provider"}
1951
+ // one context per independently-changing slice
1952
+ const ${identifier(ctxName)}Static = createContext(...);
1953
+ const ${identifier(ctxName)}${c.changedKeys.map((k) => k[0].toUpperCase() + k.slice(1)).join("")} = createContext(...);
1954
+
1955
+ // or keep one context and let consumers select a slice:
1956
+ const ${c.changedKeys[0]} = useContextSelector(${ctxName}, (v) => v.${c.changedKeys[0]});`
1694
1957
  });
1695
1958
  continue;
1696
1959
  }
1697
- if (!AVOIDABLE.has(c.kind)) continue;
1960
+ if (!import_diff2.AVOIDABLE_KINDS.has(c.kind)) continue;
1698
1961
  if (isContext) {
1699
- 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.` });
1962
+ out.push({
1963
+ kind: "contextValue",
1964
+ owner: providerOwner || `${ctxName}.Provider`,
1965
+ target: r.component,
1966
+ prop: ctxName,
1967
+ label: `memoize the ${ctxName} provider value${providerOwner ? ` in <${providerOwner}>` : ""}`,
1968
+ detail: `<${r.component}> re-rendered because ${ctxName} produced a new value that is deep-equal to the previous one${providerOwner ? ` (Provider rendered by <${providerOwner}>)` : ""}.`,
1969
+ snippet: `// ${providerOwner || `where <${ctxName}.Provider> is rendered`}
1970
+ const value = useMemo(() => ({ /* ... */ }), [/* deps */]);
1971
+ <${ctxName}.Provider value={value}>`
1972
+ });
1700
1973
  } else if (c.hook === "useSyncExternalStore") {
1701
1974
  const chain = c.custom || [];
1702
1975
  const redux = chain.some((n) => /^use(App)?Selector$/.test(n));
1703
1976
  const zustand = !redux && chain.some((n) => /^use[A-Z]\w*Store$/.test(n) || n === "useStore" || n === "useBoundStore");
1704
- 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: (0, import_report.storeAdvice)(c.custom) });
1977
+ const via = chain.length ? ` via ${chain.join(" \u203A ")}` : "";
1978
+ out.push({
1979
+ kind: "storeSnapshot",
1980
+ owner: r.component,
1981
+ target: r.component,
1982
+ prop: c.path,
1983
+ label: redux ? `memoize the selector in <${r.component}>` : zustand ? `useShallow in <${r.component}>` : `stable getSnapshot in <${r.component}>`,
1984
+ 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.`,
1985
+ snippet: redux ? `// ${r.component}
1986
+ import { shallowEqual } from 'react-redux';
1987
+ const slice = useSelector(selectSlice, shallowEqual);
1988
+ // or memoize: const selectSlice = createSelector([selectA, selectB], (a, b) => ({ a, b }));` : zustand ? `// ${r.component}
1989
+ import { useShallow } from 'zustand/react/shallow';
1990
+ const { a, b } = useStore(useShallow((s) => ({ a: s.a, b: s.b })));
1991
+ // or select a primitive: const a = useStore((s) => s.a);` : `// ${r.component}
1992
+ // getSnapshot must return the same reference while the data is unchanged
1993
+ const snapshot = useSyncExternalStore(subscribe, store.getSnapshot /* cached */);`
1994
+ });
1705
1995
  } else {
1706
- 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).` });
1996
+ out.push({
1997
+ kind: "bailout",
1998
+ owner: r.component,
1999
+ target: r.component,
2000
+ prop: c.path,
2001
+ label: `bail out before setting ${c.path} in <${r.component}>`,
2002
+ detail: `${c.path} was set to a value deep-equal to the current one (new reference).`,
2003
+ snippet: `// ${r.component}
2004
+ setState((prev) => (deepEqual(prev, next) ? prev : next));`
2005
+ });
1707
2006
  }
1708
2007
  }
1709
2008
  return out;
@@ -1717,11 +2016,12 @@ var RerenderLens = (() => {
1717
2016
  const key = fixKey(f);
1718
2017
  let agg = byKey.get(key);
1719
2018
  if (!agg) {
1720
- agg = { ...f, key, count: 0, components: {} };
2019
+ agg = { ...f, key, count: 0, components: {}, reports: [] };
1721
2020
  byKey.set(key, agg);
1722
2021
  }
1723
2022
  agg.count++;
1724
2023
  agg.components[r.component] = (agg.components[r.component] || 0) + 1;
2024
+ if (agg.reports.length < MAX_FIX_REPORTS) agg.reports.push(r);
1725
2025
  }
1726
2026
  }
1727
2027
  return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
@@ -1731,6 +2031,8 @@ var RerenderLens = (() => {
1731
2031
  if (!ranked.length) return "No avoidable re-renders.";
1732
2032
  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(", ")})`);
1733
2033
  if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
2034
+ const roots = formatRootCauses(reports, 5);
2035
+ if (roots) lines.push("", "Root causes:", roots);
1734
2036
  return lines.join("\n");
1735
2037
  }
1736
2038
 
@@ -1836,7 +2138,7 @@ ${formatFixes(reports)}`);
1836
2138
  }
1837
2139
 
1838
2140
  // src/devtools.ts
1839
- var import_diff = __toESM(require_diff(), 1);
2141
+ var import_diff3 = __toESM(require_diff(), 1);
1840
2142
  var import_tracker = __toESM(require_tracker(), 1);
1841
2143
  var import_state = __toESM(require_state(), 1);
1842
2144
  var import_fiber = __toESM(require_fiber(), 1);
@@ -1862,7 +2164,7 @@ ${formatFixes(reports)}`);
1862
2164
  const next = (v) => serialize(v, maxDepth, seen, depth + 1, budget);
1863
2165
  seen.add(obj);
1864
2166
  try {
1865
- if ((0, import_diff.isReactElement)(obj)) {
2167
+ if ((0, import_diff3.isReactElement)(obj)) {
1866
2168
  const out2 = { $type: "element", name: (0, import_tracker.getDisplayName)(obj.type) };
1867
2169
  if (obj.key !== null && obj.key !== void 0) out2.key = String(obj.key);
1868
2170
  out2.props = next(obj.props);