rerender-lens 0.3.0 → 0.4.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.
package/CHANGELOG.md CHANGED
@@ -1,7 +1,15 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.4.0
4
4
 
5
+ Highlights: the panel for any app with `npx rerender-lens panel`, Vitest and Playwright
6
+ integrations, bounded work per commit (large apps no longer freeze), a lighter panel, and the
7
+ injectable library split into three readable scripts. Details, newest first:
8
+
9
+ - **Scale test.** `examples/vite-react/scale.html?rows=N` renders N memoized cells sharing a
10
+ 2,000-item context and a 4 MB typed-array prop; its readout shows the library's overhead and
11
+ the reports skipped by the cap. The e2e suite runs it with 3,000 rows. The panel shows the
12
+ skipped count in the status tooltip and as a banner.
5
13
  - **`npx rerender-lens panel`.** A dependency-free relay (`rerender-lens/relay`) that serves the
6
14
  panel and forwards messages between apps and panels over server-sent events and POST, so the
7
15
  panel works for any app on any origin without the extension or Vite: Next.js, Webpack, a remote
@@ -18,6 +26,38 @@
18
26
  `clearReports`, `expectWithinBudget`.
19
27
  - A second copy of the library (another bundle on the same page) no longer wraps the hook
20
28
  again, which doubled every report.
29
+ - **Bounded work per commit** (large apps froze the page). Deep equality is memoized across the
30
+ components of one commit and gives up on values too large to walk (reported as `different`);
31
+ large Sets compare by identity. Serialization caps every array/object/Map/Set at 100 entries
32
+ (`…+N more`), every report at 20k nodes, treats typed arrays, `ArrayBuffer`, DOM nodes and
33
+ promises as leaves, and defaults to depth 4. A commit reports at most 200 tracked components
34
+ and stops after 25 ms; the rest is counted in `info().truncated` with a one-time warning.
35
+ Source locations are cached per element, the instance registry prunes on growth only, and
36
+ highlight/flash measure all nodes before drawing (at most 100 boxes).
37
+ - Reports are posted on `window` only once something says it listens (the extension's content
38
+ script, or `replay()`), so a page that runs the library without an open panel pays no
39
+ structured clone per report and wakes none of its own `message` listeners. `hello` and
40
+ `clear` still post; the BroadcastChannel and relay paths are unchanged.
41
+ - The injected library (extension) now runs with `includeState: false`; Settings can turn the
42
+ hook/context/state snapshots on per origin. Effect-loop detection no longer flags commits
43
+ caused by discrete input (typing, dragging). The toolbar badge repaints at most every 100 ms.
44
+ - **The panel keeps up.** Summary totals are incremental; the Offenders/Commits/Fixes lists and
45
+ the "best fix" stat update at most every 250 / 500 ms once 200 reports are buffered (the
46
+ tree and the stream stay live); commit analysis is memoized per commit and root causes are
47
+ found by index; each commit keeps at most 500 reports; buffer eviction is one splice per
48
+ frame. The content script sends one batched port message per task instead of one per
49
+ report. The DevTools panel retries the connect handshake with backoff (up to ~90 s) when a
50
+ page loads the library late, and the polling fallback clears the panel once per reset
51
+ instead of on every poll when the page overruns its buffer.
52
+ - The injectable library ships as three vendor scripts (`rerender-lens.core.js`,
53
+ `rerender-lens.engine.js`, `rerender-lens.js`) built by `scripts/build-vendor.mjs`; the
54
+ Playwright helper still gets one concatenated bundle.
55
+ - Removed: per-component notes and mute, share links (`panel.html?report=…`), the Offenders
56
+ column chooser (*Places*, *Last seen*), and the windowed Offenders/Fixes variant above 200 rows.
57
+ Offenders and Fixes always render plain rows; the tree and the stream stay virtualized. The
58
+ panel also drops its protocol-1 fallbacks: protocol 2 is the only one it speaks.
59
+ - Fix: after a navigation the panel's polling fallback stops until the new page answers `info`,
60
+ instead of evaluating into pages without the library every 500 ms.
21
61
 
22
62
  ## 0.3.0
23
63
 
package/README.md CHANGED
@@ -52,8 +52,9 @@ Then pick one of two modes:
52
52
 
53
53
  - **Inject the library** (no app code): tick *Inject the library* in the toolbar popup or in the
54
54
  panel's Settings and reload. The extension loads rerender-lens into the page before React,
55
- tracking every `React.memo` / `PureComponent` by default. Change what is tracked from
56
- Settings; the choice is saved per origin.
55
+ tracking every `React.memo` / `PureComponent` by default, without the hook/context/state
56
+ snapshots (*Include state* in Settings turns them on; they are the costliest part on big
57
+ apps). Change what is tracked from Settings; the choice is saved per origin.
57
58
  - **The page runs the library**: install the package and pass the DevTools notifier (see below).
58
59
  This is the way to go when you also want console output or want to commit the setup.
59
60
 
@@ -339,8 +340,12 @@ init({ trackAllMemoized: true, silent: true, notifier: createDevtoolsNotifier()
339
340
  ```
340
341
 
341
342
  Each report is serialized (functions become `ƒ name`, elements `<Type>`, cycles cut) and posted
342
- on `window` as `{ __rerenderLens: true, version: 2, type: 'report', payload }`. The last 300
343
- reports are buffered. The bridge at `window.__RERENDER_LENS_DEVTOOLS__` exposes:
343
+ on `window` as `{ __rerenderLens: true, version: 2, type: 'report', payload }`. Reports go on
344
+ `window` only after a listener announced itself (`{ __rerenderLensReady: true }`, which the
345
+ extension's content script posts) or after `replay()`, so a page nobody inspects pays no
346
+ structured clone per report. Values are bounded: 100 entries per array/object/Map/Set, depth 4
347
+ by default (`maxDepth`), 20k nodes per report. The last 300 reports are buffered. The bridge
348
+ at `window.__RERENDER_LENS_DEVTOOLS__` exposes:
344
349
 
345
350
  | Method | |
346
351
  | --- | --- |
@@ -392,7 +397,7 @@ deepEqual(a, b), diffRecords(prev, next), classify(prev, next)
392
397
  | `trackAllComponents` | `false` | track everything (noisy) |
393
398
  | `include` / `exclude` | | display-name matchers |
394
399
  | `trackHooks` | `true` | diff hook state and contexts |
395
- | `includeState` | `true` | put every hook, context and class state value on each report |
400
+ | `includeState` | `true` (`false` when injected by the extension) | put every hook, context and class state value on each report; the costliest option on large trees |
396
401
  | `resolveHookNames` | `false` | label hooks with the custom hooks that own them (`useCart › useState#0`); re-runs each component type once |
397
402
  | `logAll` | `false` | print non-avoidable reports too |
398
403
  | `silent` | `false` | never print; notifier still runs |
@@ -423,6 +428,10 @@ deepEqual(a, b), diffRecords(prev, next), classify(prev, next)
423
428
 
424
429
  ## Example app
425
430
 
431
+ `examples/vite-react/scale.html?rows=3000` is the scale test: thousands of memoized cells that all
432
+ re-render avoidably, sharing one large context value and a typed-array prop. Its readout shows
433
+ the library's own cost per commit and how many reports the per-commit cap skipped.
434
+
426
435
  `examples/vite-react` has three deliberate bugs. From the repo root:
427
436
 
428
437
  ```sh
@@ -115,6 +115,8 @@ interface HelloPayload {
115
115
  totalMs: number;
116
116
  maxCommitMs: number;
117
117
  };
118
+ /** Reports skipped because a commit exceeded the per-commit cap or time budget. */
119
+ truncated: number;
118
120
  }
119
121
  /** Command sent by a panel over the BroadcastChannel; answered with a `ChannelReply` of the same id. */
120
122
  interface ChannelCommand {
@@ -230,8 +232,14 @@ declare global {
230
232
  __RERENDER_LENS_DEVTOOLS__?: DevtoolsBridge;
231
233
  }
232
234
  }
233
- /** Convert a report into a structured-clone-safe value (functions, elements, cycles removed). */
234
- declare function serialize(value: unknown, maxDepth?: number, seen?: WeakSet<object>, depth?: number): unknown;
235
+ /**
236
+ * Convert a report into a structured-clone-safe value (functions, elements, cycles removed).
237
+ * Depth, breadth and total size are bounded so a giant prop (a 100k-row list, an image buffer,
238
+ * a scene graph) costs a bounded amount of time and memory per report.
239
+ */
240
+ declare function serialize(value: unknown, maxDepth?: number, seen?: WeakSet<object>, depth?: number, budget?: {
241
+ nodes: number;
242
+ }): unknown;
235
243
  /** The current options in a JSON-safe form (function matchers and custom consoles are dropped). */
236
244
  declare function serializeOptions(o: Options): SerializableOptions;
237
245
  declare function deserializeOptions(o: SerializableOptions): Options;
@@ -115,6 +115,8 @@ interface HelloPayload {
115
115
  totalMs: number;
116
116
  maxCommitMs: number;
117
117
  };
118
+ /** Reports skipped because a commit exceeded the per-commit cap or time budget. */
119
+ truncated: number;
118
120
  }
119
121
  /** Command sent by a panel over the BroadcastChannel; answered with a `ChannelReply` of the same id. */
120
122
  interface ChannelCommand {
@@ -230,8 +232,14 @@ declare global {
230
232
  __RERENDER_LENS_DEVTOOLS__?: DevtoolsBridge;
231
233
  }
232
234
  }
233
- /** Convert a report into a structured-clone-safe value (functions, elements, cycles removed). */
234
- declare function serialize(value: unknown, maxDepth?: number, seen?: WeakSet<object>, depth?: number): unknown;
235
+ /**
236
+ * Convert a report into a structured-clone-safe value (functions, elements, cycles removed).
237
+ * Depth, breadth and total size are bounded so a giant prop (a 100k-row list, an image buffer,
238
+ * a scene graph) costs a bounded amount of time and memory per report.
239
+ */
240
+ declare function serialize(value: unknown, maxDepth?: number, seen?: WeakSet<object>, depth?: number, budget?: {
241
+ nodes: number;
242
+ }): unknown;
235
243
  /** The current options in a JSON-safe form (function matchers and custom consoles are dropped). */
236
244
  declare function serializeOptions(o: Options): SerializableOptions;
237
245
  declare function deserializeOptions(o: SerializableOptions): Options;
package/dist/index.cjs CHANGED
@@ -28,10 +28,49 @@ function remember(seen, a, b) {
28
28
  set.add(b);
29
29
  return false;
30
30
  }
31
+ var scope = null;
32
+ var DEFAULT_DIFF_BUDGET = 5e4;
33
+ function beginDiffScope(budget = 2e5) {
34
+ const previous = scope;
35
+ scope = { cache: /* @__PURE__ */ new WeakMap(), remaining: budget, exhausted: false };
36
+ return () => {
37
+ scope = previous;
38
+ };
39
+ }
40
+ function diffBudgetExhausted() {
41
+ return scope?.exhausted === true;
42
+ }
31
43
  function deepEqual(a, b, seen = /* @__PURE__ */ new Map()) {
32
44
  if (Object.is(a, b)) return true;
33
45
  if (typeof a !== typeof b) return false;
34
46
  if (typeof a !== "object" || a === null || b === null) return false;
47
+ if (!scope) {
48
+ const end = beginDiffScope(DEFAULT_DIFF_BUDGET);
49
+ try {
50
+ return deepEqual(a, b, seen);
51
+ } finally {
52
+ end();
53
+ }
54
+ }
55
+ const sc = scope;
56
+ const objA = a;
57
+ const objB = b;
58
+ const hit = sc.cache.get(objA)?.get(objB);
59
+ if (hit !== void 0) return hit;
60
+ if (--sc.remaining < 0) {
61
+ sc.exhausted = true;
62
+ return false;
63
+ }
64
+ const result = deepEqualObjects(objA, objB, seen);
65
+ let m = sc.cache.get(objA);
66
+ if (!m) {
67
+ m = /* @__PURE__ */ new WeakMap();
68
+ sc.cache.set(objA, m);
69
+ }
70
+ m.set(objB, result);
71
+ return result;
72
+ }
73
+ function deepEqualObjects(a, b, seen) {
35
74
  const objA = a;
36
75
  const objB = b;
37
76
  if (remember(seen, objA, objB)) return true;
@@ -60,6 +99,10 @@ function deepEqual(a, b, seen = /* @__PURE__ */ new Map()) {
60
99
  }
61
100
  if (a instanceof Set) {
62
101
  if (!(b instanceof Set) || a.size !== b.size) return false;
102
+ if (a.size > 50) {
103
+ for (const v of a) if (!b.has(v)) return false;
104
+ return true;
105
+ }
63
106
  outer: for (const v of a) {
64
107
  if (b.has(v)) continue;
65
108
  for (const w of b) if (deepEqual(v, w, seen)) continue outer;
@@ -75,9 +118,10 @@ function deepEqual(a, b, seen = /* @__PURE__ */ new Map()) {
75
118
  const ka = Object.keys(a);
76
119
  const kb = Object.keys(b);
77
120
  if (ka.length !== kb.length) return false;
121
+ const rb = b;
78
122
  for (const k of ka) {
79
123
  if (!Object.prototype.hasOwnProperty.call(b, k)) return false;
80
- if (!deepEqual(a[k], b[k], seen)) return false;
124
+ if (!deepEqual(a[k], rb[k], seen)) return false;
81
125
  }
82
126
  return true;
83
127
  }
@@ -119,7 +163,7 @@ function diffRecords(prev, next, basePath = "") {
119
163
  if (Object.is(a, b)) continue;
120
164
  const kind = classify(a, b);
121
165
  if (kind === "different") {
122
- changes.push({ path: firstDifferentPath(a, b, path), kind, prev: a, next: b });
166
+ changes.push({ path: diffBudgetExhausted() ? path : firstDifferentPath(a, b, path), kind, prev: a, next: b });
123
167
  } else {
124
168
  changes.push({ path, kind, prev: a, next: b });
125
169
  }
@@ -330,7 +374,7 @@ function getState() {
330
374
  const g = globalThis;
331
375
  let s = g[KEY];
332
376
  if (!s) {
333
- s = { options: {}, enabled: false, printed: /* @__PURE__ */ new Map(), detach: null, nextInstanceId: 1, nextCommitId: 1, warnedOnce: /* @__PURE__ */ new Set(), scheduled: 0, commits: 0, overheadMs: 0, maxCommitMs: 0 };
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 };
334
378
  g[KEY] = s;
335
379
  }
336
380
  return s;
@@ -360,7 +404,7 @@ function dispatch(report, override) {
360
404
  try {
361
405
  options.notifier(report);
362
406
  } catch (err) {
363
- (options.console ?? console).warn("[rerender-lens] notifier threw", err);
407
+ warnOnce("notifier", `notifier threw: ${String(err)}`);
364
408
  }
365
409
  }
366
410
  }
@@ -703,11 +747,14 @@ var counts = /* @__PURE__ */ new WeakMap();
703
747
  var ids = /* @__PURE__ */ new WeakMap();
704
748
  var fibersById = /* @__PURE__ */ new Map();
705
749
  var hasWeakRef = typeof WeakRef === "function";
750
+ var pruneAt = 5e3;
706
751
  function remember2(id, fiber) {
707
752
  if (!hasWeakRef) return;
753
+ if (fibersById.get(id)?.deref() === fiber) return;
708
754
  fibersById.set(id, new WeakRef(fiber));
709
- if (fibersById.size > 5e3) {
755
+ if (fibersById.size > pruneAt) {
710
756
  for (const [k, ref] of fibersById) if (!ref.deref()) fibersById.delete(k);
757
+ pruneAt = Math.max(5e3, fibersById.size * 2);
711
758
  }
712
759
  }
713
760
  function instanceIdOf(fiber) {
@@ -762,7 +809,7 @@ function nearestComponent(fiber) {
762
809
  }
763
810
  var INTERNAL_FRAME = /react-dom|react_jsx|jsx-(dev-)?runtime|\/react\/|node_modules\/react|react-stack-bottom-frame|react_stack_bottom_frame|scheduler/;
764
811
  function parseStackLocation(stack) {
765
- for (const line of stack.split("\n")) {
812
+ for (const line of stack.split("\n", 24)) {
766
813
  const m = /(?:at\s+(?:.*?\s+)?\(?|@)?((?:https?|file|webpack|vite|blob):[^\s()]+?):(\d+):(\d+)\)?\s*$/.exec(line.trim());
767
814
  if (!m || !m[1] || INTERNAL_FRAME.test(m[1])) continue;
768
815
  return { fileName: m[1], lineNumber: Number(m[2]), columnNumber: Number(m[3]) };
@@ -778,9 +825,16 @@ function sourceOf(fiber) {
778
825
  return out;
779
826
  }
780
827
  const st = fiber._debugStack;
781
- const text = typeof st === "string" ? st : st && typeof st.stack === "string" ? st.stack : null;
782
- return text ? parseStackLocation(text) : void 0;
828
+ if (st && typeof st === "object") {
829
+ if (sourceCache.has(st)) return sourceCache.get(st);
830
+ const text = typeof st.stack === "string" ? st.stack : null;
831
+ const out = text ? parseStackLocation(text) : void 0;
832
+ sourceCache.set(st, out);
833
+ return out;
834
+ }
835
+ return typeof st === "string" ? parseStackLocation(st) : void 0;
783
836
  }
837
+ var sourceCache = /* @__PURE__ */ new WeakMap();
784
838
  function bumpCount(fiber) {
785
839
  const prev = counts.get(fiber) ?? (fiber.alternate ? counts.get(fiber.alternate) : void 0) ?? 0;
786
840
  const n = prev + 1;
@@ -968,6 +1022,8 @@ function updatersOf(root) {
968
1022
  }
969
1023
  var lastCommit = null;
970
1024
  var EFFECT_LOOP_WINDOW_MS = 50;
1025
+ var MAX_REPORTS_PER_COMMIT = 200;
1026
+ var COMMIT_TIME_BUDGET_MS = 25;
971
1027
  function onCommit(root, commitPriority) {
972
1028
  const s = getState();
973
1029
  if (!s.enabled) return;
@@ -998,7 +1054,8 @@ function onCommit(root, commitPriority) {
998
1054
  const updaters = updatersOf(root);
999
1055
  let commitCause;
1000
1056
  let afterCommit;
1001
- if (lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u))) {
1057
+ 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))) {
1002
1059
  commitCause = "effect-after-commit";
1003
1060
  afterCommit = lastCommit.id;
1004
1061
  } else if (suspenseResolved) commitCause = "suspense-resolved";
@@ -1010,47 +1067,59 @@ function onCommit(root, commitPriority) {
1010
1067
  lastCommit = { id: commitId, at, rendered: renderedNames };
1011
1068
  const dispatcherRef = o.resolveHookNames ? getDispatcherRef() : null;
1012
1069
  const parentCache = /* @__PURE__ */ new Map();
1013
- for (const fiber of rendered) {
1014
- const alt = fiber.alternate;
1015
- const a = analyze(fiber, alt, trackHooks);
1016
- const durations = durationsOf(fiber);
1017
- let hookState = o.includeState !== false && trackHooks ? snapshotHooks(fiber) : void 0;
1018
- if (dispatcherRef && fiber.tag !== ClassComponent && (a.hookChanges.length || hookState && hookState.length)) {
1019
- const names = resolveHookNames(fiber, dispatcherRef);
1020
- if (names.size) {
1021
- for (const c of a.hookChanges) if (c.hook !== "useContext" && names.has(c.index)) c.custom = names.get(c.index);
1022
- if (hookState) hookState = hookState.map((h) => names.has(h.index) ? { ...h, custom: names.get(h.index) } : h);
1070
+ const endDiffScope = beginDiffScope();
1071
+ const deadline = at + COMMIT_TIME_BUDGET_MS;
1072
+ try {
1073
+ for (let i = 0; i < rendered.length; i++) {
1074
+ if (i >= MAX_REPORTS_PER_COMMIT || (i & 15) === 15 && nowMs() > deadline) {
1075
+ s.truncated += rendered.length - i;
1076
+ warnOnce("truncated", `a commit rendered ${rendered.length} tracked components; only ${i} were reported (see info().truncated). Narrow \`include\` or turn off includeState.`);
1077
+ break;
1023
1078
  }
1079
+ const fiber = rendered[i];
1080
+ const alt = fiber.alternate;
1081
+ const a = analyze(fiber, alt, trackHooks);
1082
+ const durations = durationsOf(fiber);
1083
+ let hookState = o.includeState !== false && trackHooks ? snapshotHooks(fiber) : void 0;
1084
+ if (dispatcherRef && fiber.tag !== ClassComponent && (a.hookChanges.length || hookState && hookState.length)) {
1085
+ const names = resolveHookNames(fiber, dispatcherRef);
1086
+ if (names.size) {
1087
+ for (const c of a.hookChanges) if (c.hook !== "useContext" && names.has(c.index)) c.custom = names.get(c.index);
1088
+ if (hookState) hookState = hookState.map((h) => names.has(h.index) ? { ...h, custom: names.get(h.index) } : h);
1089
+ }
1090
+ }
1091
+ const report = buildReport({
1092
+ component: fiberName(fiber),
1093
+ instanceId: instanceId(fiber),
1094
+ renderCount: bumpCount(fiber),
1095
+ prevProps: alt.memoizedProps ?? {},
1096
+ nextProps: fiber.memoizedProps ?? {},
1097
+ propChanges: a.propChanges,
1098
+ stateChanges: a.stateChanges,
1099
+ hookChanges: a.hookChanges,
1100
+ ...o.includeState !== false ? {
1101
+ hookState,
1102
+ contexts: trackHooks ? snapshotContexts(fiber) : void 0,
1103
+ state: fiber.tag === ClassComponent && fiber.memoizedState && typeof fiber.memoizedState === "object" ? fiber.memoizedState : void 0
1104
+ } : {},
1105
+ updaters,
1106
+ commitCause,
1107
+ afterCommit,
1108
+ key: fiber.key === null || fiber.key === void 0 ? null : String(fiber.key),
1109
+ parent: a.trigger === "parent" ? nearestRenderedAncestor(fiber, parentCache, trackHooks) : null,
1110
+ owner: ownerName(fiber),
1111
+ path: componentPath(fiber),
1112
+ memoized: isMemoizedFiber(fiber),
1113
+ selfDuration: durations ? durations.self : void 0,
1114
+ treeDuration: durations ? durations.tree : void 0,
1115
+ commitId,
1116
+ commitPriority,
1117
+ source: sourceOf(fiber)
1118
+ });
1119
+ dispatch(report);
1024
1120
  }
1025
- const report = buildReport({
1026
- component: fiberName(fiber),
1027
- instanceId: instanceId(fiber),
1028
- renderCount: bumpCount(fiber),
1029
- prevProps: alt.memoizedProps ?? {},
1030
- nextProps: fiber.memoizedProps ?? {},
1031
- propChanges: a.propChanges,
1032
- stateChanges: a.stateChanges,
1033
- hookChanges: a.hookChanges,
1034
- ...o.includeState !== false ? {
1035
- hookState,
1036
- contexts: trackHooks ? snapshotContexts(fiber) : void 0,
1037
- state: fiber.tag === ClassComponent && fiber.memoizedState && typeof fiber.memoizedState === "object" ? fiber.memoizedState : void 0
1038
- } : {},
1039
- updaters,
1040
- commitCause,
1041
- afterCommit,
1042
- key: fiber.key === null || fiber.key === void 0 ? null : String(fiber.key),
1043
- parent: a.trigger === "parent" ? nearestRenderedAncestor(fiber, parentCache, trackHooks) : null,
1044
- owner: ownerName(fiber),
1045
- path: componentPath(fiber),
1046
- memoized: isMemoizedFiber(fiber),
1047
- selfDuration: durations ? durations.self : void 0,
1048
- treeDuration: durations ? durations.tree : void 0,
1049
- commitId,
1050
- commitPriority,
1051
- source: sourceOf(fiber)
1052
- });
1053
- dispatch(report);
1121
+ } finally {
1122
+ endDiffScope();
1054
1123
  }
1055
1124
  }
1056
1125
 
@@ -1444,13 +1513,24 @@ function highlight(target) {
1444
1513
  for (const s of l.sticky) s.remove();
1445
1514
  l.sticky = [];
1446
1515
  if (!target) return;
1447
- for (const node of target.nodes) {
1448
- const rect = node.getBoundingClientRect();
1449
- if (rect.width === 0 && rect.height === 0) continue;
1516
+ const frag = document.createDocumentFragment();
1517
+ for (const rect of measure(target.nodes)) {
1450
1518
  const b = box(rect, "#1a73e8", "rgba(26, 115, 232, 0.12)", target.label);
1451
- l.root.appendChild(b);
1519
+ frag.appendChild(b);
1452
1520
  l.sticky.push(b);
1453
1521
  }
1522
+ l.root.appendChild(frag);
1523
+ }
1524
+ var MAX_BOXES = 100;
1525
+ function measure(nodes) {
1526
+ const rects = [];
1527
+ for (const node of nodes) {
1528
+ if (rects.length >= MAX_BOXES) break;
1529
+ const rect = node.getBoundingClientRect();
1530
+ if (rect.width === 0 && rect.height === 0) continue;
1531
+ rects.push(rect);
1532
+ }
1533
+ return rects;
1454
1534
  }
1455
1535
  function clearHighlight() {
1456
1536
  highlight(null);
@@ -1458,27 +1538,34 @@ function clearHighlight() {
1458
1538
  function flash(target, duration = 500) {
1459
1539
  const l = ensureLayer();
1460
1540
  if (!l) return;
1461
- for (const node of target.nodes) {
1462
- const rect = node.getBoundingClientRect();
1463
- if (rect.width === 0 && rect.height === 0) continue;
1541
+ const frag = document.createDocumentFragment();
1542
+ const boxes = [];
1543
+ for (const rect of measure(target.nodes)) {
1464
1544
  const b = box(rect, "#d93025", "rgba(217, 48, 37, 0.15)");
1465
1545
  b.style.transition = `opacity ${duration}ms ease-out`;
1466
- l.root.appendChild(b);
1467
- requestAnimationFrame(() => {
1468
- b.style.opacity = "0";
1469
- });
1470
- setTimeout(() => b.remove(), duration + 50);
1546
+ frag.appendChild(b);
1547
+ boxes.push(b);
1471
1548
  }
1549
+ if (!boxes.length) return;
1550
+ l.root.appendChild(frag);
1551
+ requestAnimationFrame(() => {
1552
+ for (const b of boxes) b.style.opacity = "0";
1553
+ });
1554
+ setTimeout(() => {
1555
+ for (const b of boxes) b.remove();
1556
+ }, duration + 50);
1472
1557
  }
1473
1558
 
1474
1559
  // src/version.ts
1475
- var VERSION = "0.3.0" ;
1560
+ var VERSION = "0.4.0" ;
1476
1561
 
1477
1562
  // src/devtools.ts
1478
1563
  var DEVTOOLS_MARKER = "__rerenderLens";
1479
1564
  var PROTOCOL_VERSION = 2;
1480
1565
  var DEFAULT_CHANNEL = "rerender-lens";
1481
- function serialize(value, maxDepth = 6, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
1566
+ var SERIALIZE_MAX_ENTRIES = 100;
1567
+ var SERIALIZE_MAX_NODES = 2e4;
1568
+ function serialize(value, maxDepth = 4, seen = /* @__PURE__ */ new WeakSet(), depth = 0, budget = { nodes: SERIALIZE_MAX_NODES }) {
1482
1569
  if (value === null || value === void 0) return value;
1483
1570
  const t = typeof value;
1484
1571
  if (t === "string" || t === "boolean") return value;
@@ -1489,24 +1576,56 @@ function serialize(value, maxDepth = 6, seen = /* @__PURE__ */ new WeakSet(), de
1489
1576
  const obj = value;
1490
1577
  if (seen.has(obj)) return "[Circular]";
1491
1578
  if (depth >= maxDepth) return "[\u2026]";
1579
+ if (--budget.nodes < 0) return "[\u2026]";
1580
+ const next = (v) => serialize(v, maxDepth, seen, depth + 1, budget);
1492
1581
  seen.add(obj);
1493
1582
  try {
1494
1583
  if (isReactElement(obj)) {
1495
1584
  const out2 = { $type: "element", name: getDisplayName(obj.type) };
1496
1585
  if (obj.key !== null && obj.key !== void 0) out2.key = String(obj.key);
1497
- out2.props = serialize(obj.props, maxDepth, seen, depth + 1);
1586
+ out2.props = next(obj.props);
1498
1587
  return out2;
1499
1588
  }
1500
1589
  if (obj instanceof Date) return { $type: "Date", value: obj.toISOString() };
1501
1590
  if (obj instanceof RegExp) return { $type: "RegExp", value: String(obj) };
1591
+ if (ArrayBuffer.isView(obj) || obj instanceof ArrayBuffer) {
1592
+ const bin = obj;
1593
+ return { $type: bin.constructor?.name || "ArrayBuffer", length: typeof bin.length === "number" ? bin.length : bin.byteLength };
1594
+ }
1595
+ if (obj instanceof Promise) return "[Promise]";
1596
+ if (typeof Node !== "undefined" && obj instanceof Node) return typeof Element !== "undefined" && obj instanceof Element ? `<${obj.tagName.toLowerCase()}>` : `[${obj.nodeName}]`;
1597
+ if (obj === globalThis) return "[Window]";
1502
1598
  if (obj instanceof Map) {
1503
- return { $type: "Map", entries: [...obj].map(([k, v]) => [serialize(k, maxDepth, seen, depth + 1), serialize(v, maxDepth, seen, depth + 1)]) };
1599
+ const entries = [];
1600
+ for (const [k, v] of obj) {
1601
+ if (entries.length >= SERIALIZE_MAX_ENTRIES) {
1602
+ entries.push(["\u2026", `+${obj.size - SERIALIZE_MAX_ENTRIES} more`]);
1603
+ break;
1604
+ }
1605
+ entries.push([next(k), next(v)]);
1606
+ }
1607
+ return { $type: "Map", entries };
1608
+ }
1609
+ if (obj instanceof Set) {
1610
+ const values = [];
1611
+ for (const v of obj) {
1612
+ if (values.length >= SERIALIZE_MAX_ENTRIES) {
1613
+ values.push(`\u2026+${obj.size - SERIALIZE_MAX_ENTRIES} more`);
1614
+ break;
1615
+ }
1616
+ values.push(next(v));
1617
+ }
1618
+ return { $type: "Set", values };
1619
+ }
1620
+ if (Array.isArray(obj)) {
1621
+ const out2 = obj.slice(0, SERIALIZE_MAX_ENTRIES).map(next);
1622
+ if (obj.length > SERIALIZE_MAX_ENTRIES) out2.push(`\u2026+${obj.length - SERIALIZE_MAX_ENTRIES} more`);
1623
+ return out2;
1504
1624
  }
1505
- if (obj instanceof Set) return { $type: "Set", values: [...obj].map((v) => serialize(v, maxDepth, seen, depth + 1)) };
1506
- if (Array.isArray(obj)) return obj.map((v) => serialize(v, maxDepth, seen, depth + 1));
1507
- if (typeof Element !== "undefined" && obj instanceof Element) return `<${obj.tagName.toLowerCase()}>`;
1508
1625
  const out = {};
1509
- for (const k of Object.keys(obj)) out[k] = serialize(obj[k], maxDepth, seen, depth + 1);
1626
+ const keys = Object.keys(obj);
1627
+ for (const k of keys.slice(0, SERIALIZE_MAX_ENTRIES)) out[k] = next(obj[k]);
1628
+ if (keys.length > SERIALIZE_MAX_ENTRIES) out["\u2026"] = `+${keys.length - SERIALIZE_MAX_ENTRIES} more`;
1510
1629
  const proto = Object.getPrototypeOf(obj);
1511
1630
  if (proto && proto !== Object.prototype && proto.constructor?.name) out.$type = proto.constructor.name;
1512
1631
  return out;
@@ -1546,13 +1665,20 @@ function deserializeOptions(o) {
1546
1665
  }
1547
1666
  function createDevtoolsNotifier(options = {}) {
1548
1667
  const bufferSize = options.bufferSize ?? 300;
1549
- const maxDepth = options.maxDepth ?? 6;
1668
+ const maxDepth = options.maxDepth ?? 4;
1550
1669
  const target = options.target ?? (typeof window !== "undefined" ? window : void 0);
1551
1670
  const buffer = [];
1552
1671
  let seq = 0;
1553
1672
  let flashOn = options.flashAvoidable ?? false;
1673
+ let live = !target || typeof target.addEventListener !== "function";
1674
+ if (!live) {
1675
+ target.addEventListener("message", (event) => {
1676
+ const data = event.data;
1677
+ if ((event.source === target || !event.source) && data && data.__rerenderLensReady === true) live = true;
1678
+ });
1679
+ }
1554
1680
  const post = (type, payload) => {
1555
- if (!target) return;
1681
+ if (!target || type === "report" && !live) return;
1556
1682
  const msg = { [DEVTOOLS_MARKER]: true, version: PROTOCOL_VERSION, type, payload };
1557
1683
  target.postMessage(msg, "*");
1558
1684
  };
@@ -1569,10 +1695,12 @@ function createDevtoolsNotifier(options = {}) {
1569
1695
  injected: typeof window !== "undefined" && typeof window.__RERENDER_LENS_INJECTED__ === "string",
1570
1696
  scheduled: getState().scheduled,
1571
1697
  commits: getState().commits,
1572
- overhead: { totalMs: getState().overheadMs, maxCommitMs: getState().maxCommitMs }
1698
+ overhead: { totalMs: getState().overheadMs, maxCommitMs: getState().maxCommitMs },
1699
+ truncated: getState().truncated
1573
1700
  });
1574
1701
  const bridge = {
1575
1702
  replay: () => {
1703
+ live = true;
1576
1704
  post("hello", info());
1577
1705
  for (const e of buffer) post("report", e.payload);
1578
1706
  },