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.
- package/CHANGELOG.md +127 -0
- package/README.md +208 -359
- package/dist/{budget-LkNjGtRc.d.ts → budget-CA1MS7lB.d.cts} +20 -10
- package/dist/{budget-COBu7jBU.d.cts → budget-ok1VDJ4t.d.ts} +20 -10
- package/dist/cli.cjs +322 -53
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +322 -53
- package/dist/cli.js.map +1 -1
- package/dist/{devtools-BkCct3cJ.d.cts → devtools-Cl5n_52v.d.ts} +17 -2
- package/dist/{devtools-CZebzpF6.d.ts → devtools-UwQx2VYS.d.cts} +17 -2
- package/dist/index.cjs +537 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +137 -8
- package/dist/index.d.ts +137 -8
- package/dist/index.js +521 -50
- package/dist/index.js.map +1 -1
- package/dist/jest-setup.cjs +1690 -0
- package/dist/jest-setup.cjs.map +1 -0
- package/dist/jest-setup.d.cts +8 -0
- package/dist/jest-setup.d.ts +8 -0
- package/dist/jest-setup.js +1688 -0
- package/dist/jest-setup.js.map +1 -0
- package/dist/jest.cjs +1777 -0
- package/dist/jest.cjs.map +1 -0
- package/dist/jest.d.cts +23 -0
- package/dist/jest.d.ts +23 -0
- package/dist/jest.js +1768 -0
- package/dist/jest.js.map +1 -0
- package/dist/{notifiers-BjjGSHpp.d.ts → notifiers-B5poAEvE.d.cts} +8 -4
- package/dist/{notifiers-BGQtWKfX.d.cts → notifiers-CqQW0Eum.d.ts} +8 -4
- package/dist/playwright.cjs +230 -28
- package/dist/playwright.cjs.map +1 -1
- package/dist/playwright.d.cts +4 -4
- package/dist/playwright.d.ts +4 -4
- package/dist/playwright.js +230 -28
- package/dist/playwright.js.map +1 -1
- package/dist/relay.cjs +71 -19
- package/dist/relay.cjs.map +1 -1
- package/dist/relay.d.cts +25 -3
- package/dist/relay.d.ts +25 -3
- package/dist/relay.js +71 -19
- package/dist/relay.js.map +1 -1
- package/dist/rerender-lens.iife.js +457 -62
- package/dist/runner-report-Bvqoe89M.d.ts +46 -0
- package/dist/runner-report-CvpDg2zt.d.cts +46 -0
- package/dist/setup.cjs +209 -35
- package/dist/setup.cjs.map +1 -1
- package/dist/setup.js +209 -35
- package/dist/setup.js.map +1 -1
- package/dist/{types-BzUEVkxJ.d.cts → types-DwNN5cWe.d.cts} +82 -2
- package/dist/{types-BzUEVkxJ.d.ts → types-DwNN5cWe.d.ts} +82 -2
- package/dist/vite.d.cts +1 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vitest-setup.cjs +357 -44
- package/dist/vitest-setup.cjs.map +1 -1
- package/dist/vitest-setup.d.cts +4 -4
- package/dist/vitest-setup.d.ts +4 -4
- package/dist/vitest-setup.js +357 -44
- package/dist/vitest-setup.js.map +1 -1
- package/dist/vitest.cjs +420 -80
- package/dist/vitest.cjs.map +1 -1
- package/dist/vitest.d.cts +21 -39
- package/dist/vitest.d.ts +21 -39
- package/dist/vitest.js +421 -81
- package/dist/vitest.js.map +1 -1
- package/package.json +41 -4
- package/panel/panel.css +49 -0
- package/panel/panel.js +871 -353
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* rerender-lens 0.
|
|
1
|
+
/* rerender-lens 0.7.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.
|
|
21
|
+
var VERSION = true ? "0.7.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
|
|
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
|
|
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
|
|
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
|
-
|
|
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}.
|
|
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
|
}
|
|
@@ -406,20 +443,33 @@
|
|
|
406
443
|
// src/state.ts
|
|
407
444
|
var state_exports = {};
|
|
408
445
|
__export(state_exports, {
|
|
446
|
+
MAX_SEEN_COMPONENTS: () => MAX_SEEN_COMPONENTS,
|
|
409
447
|
dispatch: () => dispatch,
|
|
410
448
|
getState: () => getState,
|
|
449
|
+
noteRendered: () => noteRendered,
|
|
411
450
|
warnOnce: () => warnOnce
|
|
412
451
|
});
|
|
452
|
+
var MAX_SEEN_COMPONENTS = 500;
|
|
413
453
|
var KEY = /* @__PURE__ */ Symbol.for("rerender-lens.state");
|
|
414
454
|
function getState() {
|
|
415
455
|
const g = globalThis;
|
|
416
456
|
let s = g[KEY];
|
|
417
457
|
if (!s) {
|
|
418
|
-
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 };
|
|
458
|
+
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 };
|
|
419
459
|
g[KEY] = s;
|
|
420
460
|
}
|
|
421
461
|
return s;
|
|
422
462
|
}
|
|
463
|
+
function noteRendered(s, name, tracked) {
|
|
464
|
+
if (!s.seen.has(name)) {
|
|
465
|
+
if (s.seen.size >= MAX_SEEN_COMPONENTS) {
|
|
466
|
+
s.seenOverflow = true;
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
s.seen.add(name);
|
|
470
|
+
}
|
|
471
|
+
if (tracked) s.seenTracked.add(name);
|
|
472
|
+
}
|
|
423
473
|
function warnOnce(key, message) {
|
|
424
474
|
const s = getState();
|
|
425
475
|
if (s.warnedOnce.has(key)) return;
|
|
@@ -575,7 +625,7 @@
|
|
|
575
625
|
})();
|
|
576
626
|
|
|
577
627
|
;
|
|
578
|
-
/* rerender-lens 0.
|
|
628
|
+
/* rerender-lens 0.7.0 — rerender-lens.engine.js: fiber, tracker, hookNames. Built by scripts/build-vendor.mjs; do not edit. */
|
|
579
629
|
(() => {
|
|
580
630
|
var __create = Object.create;
|
|
581
631
|
var __defProp = Object.defineProperty;
|
|
@@ -660,6 +710,7 @@
|
|
|
660
710
|
getRenderers: () => getRenderers,
|
|
661
711
|
hostNodesOf: () => hostNodesOf,
|
|
662
712
|
instanceIdOf: () => instanceIdOf,
|
|
713
|
+
isCompiledFiber: () => isCompiledFiber,
|
|
663
714
|
isMemoizedFiber: () => isMemoizedFiber,
|
|
664
715
|
isProductionReact: () => isProductionReact,
|
|
665
716
|
nearestComponent: () => nearestComponent,
|
|
@@ -679,6 +730,7 @@
|
|
|
679
730
|
__export(tracker_exports, {
|
|
680
731
|
configure: () => configure,
|
|
681
732
|
disable: () => disable,
|
|
733
|
+
explainTracking: () => explainTracking,
|
|
682
734
|
getDisplayName: () => getDisplayName,
|
|
683
735
|
init: () => init,
|
|
684
736
|
isEnabled: () => isEnabled,
|
|
@@ -726,10 +778,53 @@
|
|
|
726
778
|
if (o.trackAllMemoized) return isMemo(type) || isPureClass(type);
|
|
727
779
|
return false;
|
|
728
780
|
}
|
|
781
|
+
var ANONYMOUS = "Anonymous";
|
|
782
|
+
function explainTracking(type, o) {
|
|
783
|
+
if (typeof type === "string")
|
|
784
|
+
return { tracked: false, name: type, memoized: false, reason: `<${type}> is a DOM element, not a React component; only components are tracked.` };
|
|
785
|
+
if (!isComponentLike(type))
|
|
786
|
+
return { tracked: false, name: ANONYMOUS, memoized: false, reason: "This is not a React component type, so it is never tracked." };
|
|
787
|
+
const name = getDisplayName(type);
|
|
788
|
+
const memo = isMemo(type);
|
|
789
|
+
const memoized = memo || isPureClass(type);
|
|
790
|
+
if (o.exclude?.some((m) => matches(m, name)))
|
|
791
|
+
return { tracked: false, name, memoized, reason: `<${name}> matches "exclude", which wins over every other rule.`, fix: `Remove "${name}" from the exclude list.` };
|
|
792
|
+
if (hasMarker(type)) return { tracked: true, name, memoized, reason: `<${name}> is marked with track() (the ${import_types.MARKER} static).` };
|
|
793
|
+
if (o.include?.some((m) => matches(m, name))) return { tracked: true, name, memoized, reason: `<${name}> matches "include".` };
|
|
794
|
+
if (o.trackAllComponents) return { tracked: true, name, memoized, reason: '"Track every component" is on, so every component is tracked.' };
|
|
795
|
+
if (o.trackAllMemoized && memoized)
|
|
796
|
+
return { tracked: true, name, memoized, reason: `<${name}> is ${memo ? "a React.memo component" : "a PureComponent"}, and "Track every React.memo and PureComponent" is on.` };
|
|
797
|
+
if (name === ANONYMOUS)
|
|
798
|
+
return {
|
|
799
|
+
tracked: false,
|
|
800
|
+
name,
|
|
801
|
+
memoized,
|
|
802
|
+
reason: 'This component has no display name, so "include" and "exclude" cannot address it.',
|
|
803
|
+
fix: "Give the function a name, set displayName, or mark it with track(fn, 'Name')."
|
|
804
|
+
};
|
|
805
|
+
if (o.trackAllMemoized)
|
|
806
|
+
return {
|
|
807
|
+
tracked: false,
|
|
808
|
+
name,
|
|
809
|
+
memoized,
|
|
810
|
+
reason: `"Track every React.memo and PureComponent" only covers React.memo components and PureComponent classes, and <${name}> is neither.`,
|
|
811
|
+
fix: `Add "${name}" to include, wrap it in React.memo, or turn on "Track every component".`
|
|
812
|
+
};
|
|
813
|
+
return {
|
|
814
|
+
tracked: false,
|
|
815
|
+
name,
|
|
816
|
+
memoized,
|
|
817
|
+
reason: `Nothing selects <${name}>: it is not marked with track(), no "include" pattern matches it, and neither "track every" option is on.`,
|
|
818
|
+
fix: `Add "${name}" to include, or turn on "Track every React.memo and PureComponent" / "Track every component".`
|
|
819
|
+
};
|
|
820
|
+
}
|
|
729
821
|
function init(options = {}) {
|
|
730
822
|
const s = (0, import_state.getState)();
|
|
731
823
|
s.options = { ...options };
|
|
732
824
|
s.printed.clear();
|
|
825
|
+
s.seen.clear();
|
|
826
|
+
s.seenTracked.clear();
|
|
827
|
+
s.seenOverflow = false;
|
|
733
828
|
if (!s.enabled) {
|
|
734
829
|
s.detach = attach();
|
|
735
830
|
s.enabled = true;
|
|
@@ -785,6 +880,7 @@
|
|
|
785
880
|
useEffectEvent: 1,
|
|
786
881
|
useCacheRefresh: 1
|
|
787
882
|
};
|
|
883
|
+
var BUILTIN_HOOKS = /* @__PURE__ */ new Set([...Object.keys(NODE_COUNT), "useContext", "useDebugValue", "use"]);
|
|
788
884
|
var noop = () => {
|
|
789
885
|
};
|
|
790
886
|
function customHooksFromStack(stack, componentName, marker) {
|
|
@@ -799,7 +895,7 @@
|
|
|
799
895
|
continue;
|
|
800
896
|
}
|
|
801
897
|
if (name === componentName || name.endsWith("." + componentName)) break;
|
|
802
|
-
if (/^use[A-Z0-9_]/.test(name)) names.push(name);
|
|
898
|
+
if (/^use[A-Z0-9_]/.test(name) && !BUILTIN_HOOKS.has(name)) names.push(name);
|
|
803
899
|
if (names.length > 8) break;
|
|
804
900
|
}
|
|
805
901
|
return names;
|
|
@@ -1101,6 +1197,11 @@
|
|
|
1101
1197
|
}
|
|
1102
1198
|
return false;
|
|
1103
1199
|
}
|
|
1200
|
+
function isCompiledFiber(fiber) {
|
|
1201
|
+
if (fiber.tag === ClassComponent) return false;
|
|
1202
|
+
const cache2 = fiber.updateQueue?.memoCache;
|
|
1203
|
+
return !!cache2 && Array.isArray(cache2.data);
|
|
1204
|
+
}
|
|
1104
1205
|
function durationsOf(fiber) {
|
|
1105
1206
|
if (typeof fiber.actualDuration !== "number") return null;
|
|
1106
1207
|
const tree = fiber.actualDuration;
|
|
@@ -1279,7 +1380,8 @@
|
|
|
1279
1380
|
const labels = hookLabelsFor(fiber, b);
|
|
1280
1381
|
let i = 0;
|
|
1281
1382
|
while (a && b) {
|
|
1282
|
-
|
|
1383
|
+
const deferred = labels?.[i] === "useDeferredValue";
|
|
1384
|
+
if ((deferred || isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
|
|
1283
1385
|
const hook = labels?.[i] ?? hookLabel(b);
|
|
1284
1386
|
out.push({
|
|
1285
1387
|
path: `${hook}#${i}`,
|
|
@@ -1313,6 +1415,10 @@
|
|
|
1313
1415
|
let a = alt.dependencies?.firstContext ?? null;
|
|
1314
1416
|
let b = fiber.dependencies?.firstContext ?? null;
|
|
1315
1417
|
let i = 0;
|
|
1418
|
+
if (b && !("memoizedValue" in b)) {
|
|
1419
|
+
(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.");
|
|
1420
|
+
return out;
|
|
1421
|
+
}
|
|
1316
1422
|
while (a && b) {
|
|
1317
1423
|
if (a.context === b.context && !Object.is(a.memoizedValue, b.memoizedValue)) {
|
|
1318
1424
|
const name = b.context.displayName ?? "Context";
|
|
@@ -1345,10 +1451,28 @@
|
|
|
1345
1451
|
return out;
|
|
1346
1452
|
}
|
|
1347
1453
|
var isGenuine = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
|
|
1454
|
+
var isRefObject = (v) => {
|
|
1455
|
+
if (typeof v !== "object" || v === null) return false;
|
|
1456
|
+
const keys = Object.keys(v);
|
|
1457
|
+
return keys.length === 1 && keys[0] === "current";
|
|
1458
|
+
};
|
|
1459
|
+
function diffProps(prev, next) {
|
|
1460
|
+
if (prev === next) return [];
|
|
1461
|
+
const pr = prev.ref;
|
|
1462
|
+
const nr = next.ref;
|
|
1463
|
+
if (pr !== nr && isRefObject(pr) && isRefObject(nr)) {
|
|
1464
|
+
const { ref: _p, ...restPrev } = prev;
|
|
1465
|
+
const { ref: _n, ...restNext } = next;
|
|
1466
|
+
void _p;
|
|
1467
|
+
void _n;
|
|
1468
|
+
return [{ path: "ref", kind: "deep-equal", prev: pr, next: nr }, ...(0, import_diff.diffRecords)(restPrev, restNext)];
|
|
1469
|
+
}
|
|
1470
|
+
return (0, import_diff.diffRecords)(prev, next);
|
|
1471
|
+
}
|
|
1348
1472
|
function analyze(fiber, alt, trackHooks) {
|
|
1349
1473
|
const prevProps = alt.memoizedProps ?? {};
|
|
1350
1474
|
const nextProps = fiber.memoizedProps ?? {};
|
|
1351
|
-
const propChanges =
|
|
1475
|
+
const propChanges = diffProps(prevProps, nextProps);
|
|
1352
1476
|
let stateChanges = [];
|
|
1353
1477
|
let hookChanges = [];
|
|
1354
1478
|
if (fiber.tag === ClassComponent) {
|
|
@@ -1361,7 +1485,7 @@
|
|
|
1361
1485
|
} else if (trackHooks) {
|
|
1362
1486
|
hookChanges = [...diffHooks(fiber, alt), ...diffContexts(fiber, alt)];
|
|
1363
1487
|
}
|
|
1364
|
-
const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore";
|
|
1488
|
+
const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore" && h.hook !== "useDeferredValue";
|
|
1365
1489
|
const causes = [];
|
|
1366
1490
|
if (propChanges.some(isGenuine)) causes.push("props");
|
|
1367
1491
|
if (stateChanges.some(isGenuine) || hookChanges.some((h) => isStateHook(h) && isGenuine(h))) causes.push("state");
|
|
@@ -1401,6 +1525,24 @@
|
|
|
1401
1525
|
var EFFECT_LOOP_WINDOW_MS = 50;
|
|
1402
1526
|
var MAX_REPORTS_PER_COMMIT = 200;
|
|
1403
1527
|
var COMMIT_TIME_BUDGET_MS = 25;
|
|
1528
|
+
function isDeferredCatchUp(fiber) {
|
|
1529
|
+
const types = fiber._debugHookTypes;
|
|
1530
|
+
if (!types || !types.includes("useDeferredValue") || !fiber.alternate) return false;
|
|
1531
|
+
const first = fiber.memoizedState;
|
|
1532
|
+
if (!isHookList(first)) return false;
|
|
1533
|
+
const labels = hookLabelsFor(fiber, first);
|
|
1534
|
+
if (!labels) return false;
|
|
1535
|
+
let a = fiber.alternate.memoizedState;
|
|
1536
|
+
let b = first;
|
|
1537
|
+
for (let i = 0; a && b; a = a.next, b = b.next, i++) {
|
|
1538
|
+
if (labels[i] === "useDeferredValue" && !Object.is(a.memoizedState, b.memoizedState)) return true;
|
|
1539
|
+
}
|
|
1540
|
+
return false;
|
|
1541
|
+
}
|
|
1542
|
+
function isUnder(fiber, boundaries) {
|
|
1543
|
+
for (let f = fiber.return; f && f.tag !== HostRoot; f = f.return) if (f.tag === SuspenseComponent && boundaries.includes(f)) return true;
|
|
1544
|
+
return false;
|
|
1545
|
+
}
|
|
1404
1546
|
function onCommit(root, commitPriority) {
|
|
1405
1547
|
const s = (0, import_state2.getState)();
|
|
1406
1548
|
if (!s.enabled) return;
|
|
@@ -1410,17 +1552,20 @@
|
|
|
1410
1552
|
const rendered = [];
|
|
1411
1553
|
const renderedNames = /* @__PURE__ */ new Set();
|
|
1412
1554
|
let hot = false;
|
|
1413
|
-
let
|
|
1555
|
+
let resolvedBoundaries = null;
|
|
1414
1556
|
const stack = [root.current];
|
|
1415
1557
|
while (stack.length) {
|
|
1416
1558
|
const fiber = stack.pop();
|
|
1417
1559
|
const alt = fiber.alternate;
|
|
1418
1560
|
if (alt && isComponentTag(fiber.tag) && didRender(fiber)) {
|
|
1419
1561
|
if (isHotSwapped(fiber, alt)) hot = true;
|
|
1420
|
-
|
|
1421
|
-
|
|
1562
|
+
const name = fiberName(fiber);
|
|
1563
|
+
renderedNames.add(name);
|
|
1564
|
+
const tracked = shouldTrack(fiberType(fiber), o);
|
|
1565
|
+
(0, import_state2.noteRendered)(s, name, tracked);
|
|
1566
|
+
if (tracked) rendered.push(fiber);
|
|
1422
1567
|
}
|
|
1423
|
-
if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null)
|
|
1568
|
+
if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) (resolvedBoundaries ?? (resolvedBoundaries = [])).push(fiber);
|
|
1424
1569
|
if (!alt || fiber.child !== alt.child) {
|
|
1425
1570
|
for (let c = fiber.child; c; c = c.sibling) stack.push(c);
|
|
1426
1571
|
}
|
|
@@ -1432,10 +1577,10 @@
|
|
|
1432
1577
|
let commitCause;
|
|
1433
1578
|
let afterCommit;
|
|
1434
1579
|
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))) {
|
|
1580
|
+
if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u)) && !rendered.some(isDeferredCatchUp)) {
|
|
1436
1581
|
commitCause = "effect-after-commit";
|
|
1437
1582
|
afterCommit = lastCommit.id;
|
|
1438
|
-
} else if (
|
|
1583
|
+
} else if (resolvedBoundaries) commitCause = "suspense-resolved";
|
|
1439
1584
|
if (rendered.length === 0) {
|
|
1440
1585
|
lastCommit = { id: s.nextCommitId, at, rendered: renderedNames };
|
|
1441
1586
|
return;
|
|
@@ -1487,6 +1632,9 @@
|
|
|
1487
1632
|
owner: ownerName(fiber),
|
|
1488
1633
|
path: componentPath(fiber),
|
|
1489
1634
|
memoized: isMemoizedFiber(fiber),
|
|
1635
|
+
compiled: isCompiledFiber(fiber),
|
|
1636
|
+
classComponent: fiber.tag === ClassComponent,
|
|
1637
|
+
revealed: resolvedBoundaries !== null && a.trigger === "parent" && isUnder(fiber, resolvedBoundaries),
|
|
1490
1638
|
selfDuration: durations ? durations.self : void 0,
|
|
1491
1639
|
treeDuration: durations ? durations.tree : void 0,
|
|
1492
1640
|
commitId,
|
|
@@ -1508,7 +1656,7 @@
|
|
|
1508
1656
|
})();
|
|
1509
1657
|
|
|
1510
1658
|
;
|
|
1511
|
-
/* rerender-lens 0.
|
|
1659
|
+
/* rerender-lens 0.7.0 — rerender-lens.js: devtools bridge + notifiers (window.RerenderLens). Built by scripts/build-vendor.mjs; do not edit. */
|
|
1512
1660
|
"use strict";
|
|
1513
1661
|
var RerenderLens = (() => {
|
|
1514
1662
|
var __create = Object.create;
|
|
@@ -1621,66 +1769,235 @@ var RerenderLens = (() => {
|
|
|
1621
1769
|
MARKER: () => import_types.MARKER,
|
|
1622
1770
|
PROTOCOL_VERSION: () => PROTOCOL_VERSION,
|
|
1623
1771
|
VERSION: () => import_version2.VERSION,
|
|
1624
|
-
buildReport: () =>
|
|
1625
|
-
classify: () =>
|
|
1772
|
+
buildReport: () => import_report.buildReport,
|
|
1773
|
+
classify: () => import_diff4.classify,
|
|
1626
1774
|
combineNotifiers: () => combineNotifiers,
|
|
1627
1775
|
configure: () => import_tracker2.configure,
|
|
1628
1776
|
createCollector: () => createCollector,
|
|
1629
1777
|
createDevtoolsNotifier: () => createDevtoolsNotifier,
|
|
1630
|
-
deepEqual: () =>
|
|
1631
|
-
diffRecords: () =>
|
|
1778
|
+
deepEqual: () => import_diff4.deepEqual,
|
|
1779
|
+
diffRecords: () => import_diff4.diffRecords,
|
|
1632
1780
|
disable: () => import_tracker2.disable,
|
|
1633
1781
|
ensureDevtoolsHook: () => import_fiber2.ensureDevtoolsHook,
|
|
1782
|
+
explainTracking: () => import_tracker2.explainTracking,
|
|
1634
1783
|
getDisplayName: () => import_tracker2.getDisplayName,
|
|
1635
1784
|
getRenderers: () => import_fiber2.getRenderers,
|
|
1636
1785
|
init: () => import_tracker2.init,
|
|
1637
1786
|
isEnabled: () => import_tracker2.isEnabled,
|
|
1638
1787
|
isProductionReact: () => import_fiber2.isProductionReact,
|
|
1639
|
-
printReport: () =>
|
|
1788
|
+
printReport: () => import_report.printReport,
|
|
1640
1789
|
serialize: () => serialize,
|
|
1641
1790
|
shouldTrack: () => import_tracker2.shouldTrack,
|
|
1642
|
-
summarize: () =>
|
|
1791
|
+
summarize: () => import_report.summarize,
|
|
1643
1792
|
track: () => import_tracker2.track
|
|
1644
1793
|
});
|
|
1645
1794
|
var import_types = __toESM(require_types(), 1);
|
|
1646
1795
|
var import_tracker2 = __toESM(require_tracker(), 1);
|
|
1647
1796
|
var import_fiber2 = __toESM(require_fiber(), 1);
|
|
1797
|
+
var import_diff4 = __toESM(require_diff(), 1);
|
|
1798
|
+
var import_report = __toESM(require_report(), 1);
|
|
1799
|
+
|
|
1800
|
+
// src/fixes.ts
|
|
1648
1801
|
var import_diff2 = __toESM(require_diff(), 1);
|
|
1649
|
-
|
|
1802
|
+
|
|
1803
|
+
// src/causes.ts
|
|
1804
|
+
var import_diff = __toESM(require_diff(), 1);
|
|
1805
|
+
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);
|
|
1806
|
+
var bump = (m, key, n = 1) => void m.set(key, (m.get(key) || 0) + n);
|
|
1807
|
+
function indexByComponent(reports) {
|
|
1808
|
+
const index = /* @__PURE__ */ new Map();
|
|
1809
|
+
for (const r of reports) {
|
|
1810
|
+
const list = index.get(r.component);
|
|
1811
|
+
if (list) list.push(r);
|
|
1812
|
+
else index.set(r.component, [r]);
|
|
1813
|
+
}
|
|
1814
|
+
return index;
|
|
1815
|
+
}
|
|
1816
|
+
function rootCauseOf(r, commitReports, index = indexByComponent(commitReports)) {
|
|
1817
|
+
let cur = r;
|
|
1818
|
+
const seen = /* @__PURE__ */ new Set([r]);
|
|
1819
|
+
while (cur.trigger === "parent" && cur.parent) {
|
|
1820
|
+
const parent = cur.parent;
|
|
1821
|
+
const candidates = index.get(parent.name);
|
|
1822
|
+
const p = candidates && candidates.find((x) => isAncestorReport(x, cur));
|
|
1823
|
+
if (!p || seen.has(p)) return { name: parent.name, trigger: parent.trigger, report: null };
|
|
1824
|
+
seen.add(p);
|
|
1825
|
+
cur = p;
|
|
1826
|
+
}
|
|
1827
|
+
return cur === r ? null : { name: cur.component, trigger: cur.trigger, report: cur };
|
|
1828
|
+
}
|
|
1829
|
+
function rootCausesOf(reports) {
|
|
1830
|
+
const roots = /* @__PURE__ */ new Map();
|
|
1831
|
+
const rootByReport = /* @__PURE__ */ new Map();
|
|
1832
|
+
const index = indexByComponent(reports);
|
|
1833
|
+
let avoidable = 0;
|
|
1834
|
+
let wasted = 0;
|
|
1835
|
+
for (const r of reports) {
|
|
1836
|
+
if (!r.avoidable) continue;
|
|
1837
|
+
avoidable++;
|
|
1838
|
+
if (typeof r.selfDuration === "number") wasted += r.selfDuration;
|
|
1839
|
+
const root = rootCauseOf(r, reports, index);
|
|
1840
|
+
const name = root ? root.name : r.parent && r.parent.name || "(unknown)";
|
|
1841
|
+
const trigger = root ? root.trigger : r.parent && r.parent.trigger || "parent";
|
|
1842
|
+
rootByReport.set(r, name);
|
|
1843
|
+
let agg = roots.get(name);
|
|
1844
|
+
if (!agg) {
|
|
1845
|
+
agg = { name, trigger, count: 0, components: /* @__PURE__ */ new Map() };
|
|
1846
|
+
roots.set(name, agg);
|
|
1847
|
+
}
|
|
1848
|
+
agg.count++;
|
|
1849
|
+
bump(agg.components, r.component);
|
|
1850
|
+
}
|
|
1851
|
+
return { roots: [...roots.values()].sort((a, b) => b.count - a.count), rootByReport, avoidable, wasted };
|
|
1852
|
+
}
|
|
1853
|
+
function groupByCommit(reports) {
|
|
1854
|
+
const out = /* @__PURE__ */ new Map();
|
|
1855
|
+
for (const r of reports) {
|
|
1856
|
+
if (typeof r.commitId !== "number" || r.commitId <= 0) continue;
|
|
1857
|
+
const list = out.get(r.commitId);
|
|
1858
|
+
if (list) list.push(r);
|
|
1859
|
+
else out.set(r.commitId, [r]);
|
|
1860
|
+
}
|
|
1861
|
+
return out;
|
|
1862
|
+
}
|
|
1863
|
+
function rankRootCauses(reports) {
|
|
1864
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1865
|
+
for (const commit of groupByCommit(reports).values()) {
|
|
1866
|
+
for (const root of rootCausesOf(commit).roots) {
|
|
1867
|
+
let agg = byName.get(root.name);
|
|
1868
|
+
if (!agg) {
|
|
1869
|
+
agg = { name: root.name, trigger: root.trigger, commits: 0, count: 0, components: /* @__PURE__ */ new Map() };
|
|
1870
|
+
byName.set(root.name, agg);
|
|
1871
|
+
}
|
|
1872
|
+
agg.commits++;
|
|
1873
|
+
agg.count += root.count;
|
|
1874
|
+
for (const [c, n] of root.components) bump(agg.components, c, n);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
return [...byName.values()].sort((a, b) => b.count - a.count || b.commits - a.commits || a.name.localeCompare(b.name));
|
|
1878
|
+
}
|
|
1879
|
+
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
1880
|
+
function formatRootCauses(reports, limit = 5) {
|
|
1881
|
+
const ranked = rankRootCauses(reports);
|
|
1882
|
+
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(", ")})`);
|
|
1883
|
+
if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
|
|
1884
|
+
return lines.join("\n");
|
|
1885
|
+
}
|
|
1650
1886
|
|
|
1651
1887
|
// src/fixes.ts
|
|
1652
|
-
var
|
|
1653
|
-
var AVOIDABLE = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
|
|
1888
|
+
var MAX_FIX_REPORTS = 50;
|
|
1654
1889
|
var rootOf = (path) => path.split(/[.[]/)[0] || path;
|
|
1890
|
+
var identifier = (name) => /^[A-Za-z_$][\w$]*$/.test(name) ? name : "value";
|
|
1891
|
+
function shortValue(v, max = 60) {
|
|
1892
|
+
let s;
|
|
1893
|
+
try {
|
|
1894
|
+
s = JSON.stringify(v);
|
|
1895
|
+
} catch {
|
|
1896
|
+
s = String(v);
|
|
1897
|
+
}
|
|
1898
|
+
if (s === void 0) s = String(v);
|
|
1899
|
+
return s.length > max ? s.slice(0, max - 3) + "..." : s;
|
|
1900
|
+
}
|
|
1655
1901
|
function fixesFor(r) {
|
|
1656
1902
|
const out = [];
|
|
1657
|
-
const
|
|
1658
|
-
const
|
|
1903
|
+
const ownerName = r.owner || r.parent && r.parent.name || null;
|
|
1904
|
+
const propChanges = r.propChanges || [];
|
|
1905
|
+
const changes = [...propChanges, ...r.stateChanges || [], ...r.hookChanges || []];
|
|
1659
1906
|
if (r.avoidable && (changes.length === 0 || r.memoized === false)) {
|
|
1907
|
+
const identical = changes.length === 0;
|
|
1660
1908
|
out.push({
|
|
1661
1909
|
kind: "memo",
|
|
1662
1910
|
owner: r.component,
|
|
1663
1911
|
target: r.component,
|
|
1664
1912
|
prop: null,
|
|
1665
1913
|
label: `Wrap <${r.component}> in React.memo`,
|
|
1666
|
-
detail:
|
|
1914
|
+
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.`,
|
|
1915
|
+
snippet: `// ${r.component}
|
|
1916
|
+
import { memo } from 'react';
|
|
1917
|
+
|
|
1918
|
+
export const ${r.component} = memo(function ${r.component}(props) {
|
|
1919
|
+
// ...
|
|
1920
|
+
});
|
|
1921
|
+
// class components: extend PureComponent instead`
|
|
1667
1922
|
});
|
|
1668
1923
|
}
|
|
1669
|
-
for (const c of
|
|
1670
|
-
if (!
|
|
1924
|
+
for (const c of propChanges) {
|
|
1925
|
+
if (!import_diff2.AVOIDABLE_KINDS.has(c.kind)) continue;
|
|
1926
|
+
const owner = ownerName || "?";
|
|
1671
1927
|
const root = rootOf(c.path);
|
|
1928
|
+
const id = identifier(root);
|
|
1672
1929
|
if (root === "children" && (c.kind === "element" || c.kind === "deep-equal")) {
|
|
1673
|
-
out.push({
|
|
1930
|
+
out.push({
|
|
1931
|
+
kind: "children",
|
|
1932
|
+
owner,
|
|
1933
|
+
target: r.component,
|
|
1934
|
+
prop: "children",
|
|
1935
|
+
label: `memoize children of <${r.component}> in <${owner}>`,
|
|
1936
|
+
detail: `<${owner}> re-creates the children of <${r.component}> on every render; they have the same types and props each time.`,
|
|
1937
|
+
snippet: `// ${owner}
|
|
1938
|
+
import { useMemo } from 'react';
|
|
1939
|
+
|
|
1940
|
+
const children = useMemo(() => (
|
|
1941
|
+
<>{/* the same elements */}</>
|
|
1942
|
+
), [/* deps */]);
|
|
1943
|
+
|
|
1944
|
+
<${r.component}>{children}</${r.component}>
|
|
1945
|
+
|
|
1946
|
+
// static children: hoist them to module scope
|
|
1947
|
+
const STATIC = <em>hi</em>;`
|
|
1948
|
+
});
|
|
1674
1949
|
} else if (c.kind === "function") {
|
|
1675
|
-
out.push({
|
|
1950
|
+
out.push({
|
|
1951
|
+
kind: "useCallback",
|
|
1952
|
+
owner,
|
|
1953
|
+
target: r.component,
|
|
1954
|
+
prop: root,
|
|
1955
|
+
label: `useCallback(${root}) in <${owner}>`,
|
|
1956
|
+
detail: `prop "${c.path}" of <${r.component}> is a new function on every render of <${owner}>.`,
|
|
1957
|
+
snippet: `// ${owner}
|
|
1958
|
+
import { useCallback } from 'react';
|
|
1959
|
+
|
|
1960
|
+
const ${id} = useCallback((/* args */) => {
|
|
1961
|
+
// ...
|
|
1962
|
+
}, [/* deps */]);
|
|
1963
|
+
|
|
1964
|
+
<${r.component} ${root}={${id}} />`
|
|
1965
|
+
});
|
|
1676
1966
|
} else if (c.kind === "element") {
|
|
1677
|
-
out.push({
|
|
1967
|
+
out.push({
|
|
1968
|
+
kind: "useMemoElement",
|
|
1969
|
+
owner,
|
|
1970
|
+
target: r.component,
|
|
1971
|
+
prop: root,
|
|
1972
|
+
label: `memoize element prop ${root} in <${owner}>`,
|
|
1973
|
+
detail: `prop "${c.path}" of <${r.component}> is a new element with the same type and props on every render of <${owner}>.`,
|
|
1974
|
+
snippet: `// ${owner}
|
|
1975
|
+
import { useMemo } from 'react';
|
|
1976
|
+
|
|
1977
|
+
const ${id} = useMemo(() => ${shortValue(c.next, 40)}, [/* deps */]);
|
|
1978
|
+
// or pass it as children from a component that does not re-render`
|
|
1979
|
+
});
|
|
1678
1980
|
} else {
|
|
1679
|
-
|
|
1981
|
+
const isArray = Array.isArray(c.next);
|
|
1982
|
+
out.push({
|
|
1983
|
+
kind: "useMemo",
|
|
1984
|
+
owner,
|
|
1985
|
+
target: r.component,
|
|
1986
|
+
prop: root,
|
|
1987
|
+
label: `useMemo(${root}) in <${owner}>`,
|
|
1988
|
+
detail: `prop "${c.path}" of <${r.component}> is a new ${isArray ? "array" : "object"} with the same contents on every render of <${owner}>.`,
|
|
1989
|
+
snippet: `// ${owner}
|
|
1990
|
+
import { useMemo } from 'react';
|
|
1991
|
+
|
|
1992
|
+
const ${id} = useMemo(() => (${shortValue(c.next, 80)}), [/* deps */]);
|
|
1993
|
+
|
|
1994
|
+
// or, when it never changes, hoist it to module scope:
|
|
1995
|
+
const ${id.toUpperCase()} = ${shortValue(c.next, 80)};`
|
|
1996
|
+
});
|
|
1680
1997
|
}
|
|
1681
1998
|
}
|
|
1682
|
-
for (const c of [...r.stateChanges, ...r.hookChanges]) {
|
|
1683
|
-
const isContext = c.hook === "useContext";
|
|
1999
|
+
for (const c of [...r.stateChanges || [], ...r.hookChanges || []]) {
|
|
2000
|
+
const isContext = c.hook === "useContext" || /^useContext/.test(c.path);
|
|
1684
2001
|
const ctxName = isContext ? (/useContext\((.*)\)/.exec(c.path) || [])[1] || "Context" : "";
|
|
1685
2002
|
const providerOwner = isContext && c.provider && c.provider.component ? c.provider.component : null;
|
|
1686
2003
|
if (isContext && c.kind === "different" && c.changedKeys && typeof c.totalKeys === "number" && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys) {
|
|
@@ -1690,20 +2007,63 @@ var RerenderLens = (() => {
|
|
|
1690
2007
|
target: r.component,
|
|
1691
2008
|
prop: ctxName,
|
|
1692
2009
|
label: `split ${ctxName}${providerOwner ? ` in <${providerOwner}>` : ""}: only ${c.changedKeys.join(", ")} changed`,
|
|
1693
|
-
detail: `${c.changedKeys.
|
|
2010
|
+
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.`,
|
|
2011
|
+
snippet: `// ${providerOwner || "Provider"}
|
|
2012
|
+
// one context per independently-changing slice
|
|
2013
|
+
const ${identifier(ctxName)}Static = createContext(...);
|
|
2014
|
+
const ${identifier(ctxName)}${c.changedKeys.map((k) => k[0].toUpperCase() + k.slice(1)).join("")} = createContext(...);
|
|
2015
|
+
|
|
2016
|
+
// or keep one context and let consumers select a slice:
|
|
2017
|
+
const ${c.changedKeys[0]} = useContextSelector(${ctxName}, (v) => v.${c.changedKeys[0]});`
|
|
1694
2018
|
});
|
|
1695
2019
|
continue;
|
|
1696
2020
|
}
|
|
1697
|
-
if (!
|
|
2021
|
+
if (!import_diff2.AVOIDABLE_KINDS.has(c.kind)) continue;
|
|
1698
2022
|
if (isContext) {
|
|
1699
|
-
out.push({
|
|
2023
|
+
out.push({
|
|
2024
|
+
kind: "contextValue",
|
|
2025
|
+
owner: providerOwner || `${ctxName}.Provider`,
|
|
2026
|
+
target: r.component,
|
|
2027
|
+
prop: ctxName,
|
|
2028
|
+
label: `memoize the ${ctxName} provider value${providerOwner ? ` in <${providerOwner}>` : ""}`,
|
|
2029
|
+
detail: `<${r.component}> re-rendered because ${ctxName} produced a new value that is deep-equal to the previous one${providerOwner ? ` (Provider rendered by <${providerOwner}>)` : ""}.`,
|
|
2030
|
+
snippet: `// ${providerOwner || `where <${ctxName}.Provider> is rendered`}
|
|
2031
|
+
const value = useMemo(() => ({ /* ... */ }), [/* deps */]);
|
|
2032
|
+
<${ctxName}.Provider value={value}>`
|
|
2033
|
+
});
|
|
1700
2034
|
} else if (c.hook === "useSyncExternalStore") {
|
|
1701
2035
|
const chain = c.custom || [];
|
|
1702
2036
|
const redux = chain.some((n) => /^use(App)?Selector$/.test(n));
|
|
1703
2037
|
const zustand = !redux && chain.some((n) => /^use[A-Z]\w*Store$/.test(n) || n === "useStore" || n === "useBoundStore");
|
|
1704
|
-
|
|
2038
|
+
const via = chain.length ? ` via ${chain.join(" \u203A ")}` : "";
|
|
2039
|
+
out.push({
|
|
2040
|
+
kind: "storeSnapshot",
|
|
2041
|
+
owner: r.component,
|
|
2042
|
+
target: r.component,
|
|
2043
|
+
prop: c.path,
|
|
2044
|
+
label: redux ? `memoize the selector in <${r.component}>` : zustand ? `useShallow in <${r.component}>` : `stable getSnapshot in <${r.component}>`,
|
|
2045
|
+
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.`,
|
|
2046
|
+
snippet: redux ? `// ${r.component}
|
|
2047
|
+
import { shallowEqual } from 'react-redux';
|
|
2048
|
+
const slice = useSelector(selectSlice, shallowEqual);
|
|
2049
|
+
// or memoize: const selectSlice = createSelector([selectA, selectB], (a, b) => ({ a, b }));` : zustand ? `// ${r.component}
|
|
2050
|
+
import { useShallow } from 'zustand/react/shallow';
|
|
2051
|
+
const { a, b } = useStore(useShallow((s) => ({ a: s.a, b: s.b })));
|
|
2052
|
+
// or select a primitive: const a = useStore((s) => s.a);` : `// ${r.component}
|
|
2053
|
+
// getSnapshot must return the same reference while the data is unchanged
|
|
2054
|
+
const snapshot = useSyncExternalStore(subscribe, store.getSnapshot /* cached */);`
|
|
2055
|
+
});
|
|
1705
2056
|
} else {
|
|
1706
|
-
out.push({
|
|
2057
|
+
out.push({
|
|
2058
|
+
kind: "bailout",
|
|
2059
|
+
owner: r.component,
|
|
2060
|
+
target: r.component,
|
|
2061
|
+
prop: c.path,
|
|
2062
|
+
label: `bail out before setting ${c.path} in <${r.component}>`,
|
|
2063
|
+
detail: `${c.path} was set to a value deep-equal to the current one (new reference).`,
|
|
2064
|
+
snippet: `// ${r.component}
|
|
2065
|
+
setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
2066
|
+
});
|
|
1707
2067
|
}
|
|
1708
2068
|
}
|
|
1709
2069
|
return out;
|
|
@@ -1717,11 +2077,12 @@ var RerenderLens = (() => {
|
|
|
1717
2077
|
const key = fixKey(f);
|
|
1718
2078
|
let agg = byKey.get(key);
|
|
1719
2079
|
if (!agg) {
|
|
1720
|
-
agg = { ...f, key, count: 0, components: {} };
|
|
2080
|
+
agg = { ...f, key, count: 0, components: {}, reports: [] };
|
|
1721
2081
|
byKey.set(key, agg);
|
|
1722
2082
|
}
|
|
1723
2083
|
agg.count++;
|
|
1724
2084
|
agg.components[r.component] = (agg.components[r.component] || 0) + 1;
|
|
2085
|
+
if (agg.reports.length < MAX_FIX_REPORTS) agg.reports.push(r);
|
|
1725
2086
|
}
|
|
1726
2087
|
}
|
|
1727
2088
|
return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
|
|
@@ -1731,6 +2092,8 @@ var RerenderLens = (() => {
|
|
|
1731
2092
|
if (!ranked.length) return "No avoidable re-renders.";
|
|
1732
2093
|
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
2094
|
if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
|
|
2095
|
+
const roots = formatRootCauses(reports, 5);
|
|
2096
|
+
if (roots) lines.push("", "Root causes:", roots);
|
|
1734
2097
|
return lines.join("\n");
|
|
1735
2098
|
}
|
|
1736
2099
|
|
|
@@ -1836,7 +2199,7 @@ ${formatFixes(reports)}`);
|
|
|
1836
2199
|
}
|
|
1837
2200
|
|
|
1838
2201
|
// src/devtools.ts
|
|
1839
|
-
var
|
|
2202
|
+
var import_diff3 = __toESM(require_diff(), 1);
|
|
1840
2203
|
var import_tracker = __toESM(require_tracker(), 1);
|
|
1841
2204
|
var import_state = __toESM(require_state(), 1);
|
|
1842
2205
|
var import_fiber = __toESM(require_fiber(), 1);
|
|
@@ -1862,7 +2225,7 @@ ${formatFixes(reports)}`);
|
|
|
1862
2225
|
const next = (v) => serialize(v, maxDepth, seen, depth + 1, budget);
|
|
1863
2226
|
seen.add(obj);
|
|
1864
2227
|
try {
|
|
1865
|
-
if ((0,
|
|
2228
|
+
if ((0, import_diff3.isReactElement)(obj)) {
|
|
1866
2229
|
const out2 = { $type: "element", name: (0, import_tracker.getDisplayName)(obj.type) };
|
|
1867
2230
|
if (obj.key !== null && obj.key !== void 0) out2.key = String(obj.key);
|
|
1868
2231
|
out2.props = next(obj.props);
|
|
@@ -1965,6 +2328,18 @@ ${formatFixes(reports)}`);
|
|
|
1965
2328
|
target.postMessage(msg, "*");
|
|
1966
2329
|
};
|
|
1967
2330
|
const source = options.source ?? "page";
|
|
2331
|
+
const trackingSummary = () => {
|
|
2332
|
+
const s = (0, import_state.getState)();
|
|
2333
|
+
const o = s.options;
|
|
2334
|
+
return {
|
|
2335
|
+
mode: o.trackAllComponents ? "all" : o.trackAllMemoized ? "memoized" : "marked",
|
|
2336
|
+
include: (o.include ?? []).map(matcherToString).filter((x) => x !== null),
|
|
2337
|
+
exclude: (o.exclude ?? []).map(matcherToString).filter((x) => x !== null),
|
|
2338
|
+
renderedCount: s.seen.size,
|
|
2339
|
+
trackedCount: s.seenTracked.size,
|
|
2340
|
+
overflow: s.seenOverflow
|
|
2341
|
+
};
|
|
2342
|
+
};
|
|
1968
2343
|
const info = () => ({
|
|
1969
2344
|
count: buffer.length,
|
|
1970
2345
|
library: import_version.VERSION,
|
|
@@ -1978,7 +2353,8 @@ ${formatFixes(reports)}`);
|
|
|
1978
2353
|
scheduled: (0, import_state.getState)().scheduled,
|
|
1979
2354
|
commits: (0, import_state.getState)().commits,
|
|
1980
2355
|
overhead: { totalMs: (0, import_state.getState)().overheadMs, maxCommitMs: (0, import_state.getState)().maxCommitMs },
|
|
1981
|
-
truncated: (0, import_state.getState)().truncated
|
|
2356
|
+
truncated: (0, import_state.getState)().truncated,
|
|
2357
|
+
tracking: trackingSummary()
|
|
1982
2358
|
});
|
|
1983
2359
|
const bridge = {
|
|
1984
2360
|
replay: () => {
|
|
@@ -2024,6 +2400,10 @@ ${formatFixes(reports)}`);
|
|
|
2024
2400
|
flashAvoidable: (on) => {
|
|
2025
2401
|
flashOn = !!on;
|
|
2026
2402
|
},
|
|
2403
|
+
explain: (target2) => {
|
|
2404
|
+
const comp = typeof target2 === "number" ? (0, import_fiber.fiberById)(target2) : (0, import_fiber.nearestComponent)((0, import_fiber.fiberForNode)(target2));
|
|
2405
|
+
return comp ? (0, import_tracker.explainTracking)((0, import_fiber.fiberType)(comp), (0, import_state.getState)().options) : null;
|
|
2406
|
+
},
|
|
2027
2407
|
inspect: (node) => {
|
|
2028
2408
|
const comp = (0, import_fiber.nearestComponent)((0, import_fiber.fiberForNode)(node));
|
|
2029
2409
|
if (!comp) return null;
|
|
@@ -2038,6 +2418,7 @@ ${formatFixes(reports)}`);
|
|
|
2038
2418
|
component: (0, import_fiber.fiberName)(comp),
|
|
2039
2419
|
instanceId: id,
|
|
2040
2420
|
tracked: (0, import_tracker.shouldTrack)((0, import_fiber.fiberType)(comp), (0, import_state.getState)().options),
|
|
2421
|
+
tracking: (0, import_tracker.explainTracking)((0, import_fiber.fiberType)(comp), (0, import_state.getState)().options),
|
|
2041
2422
|
path,
|
|
2042
2423
|
reports: id === null ? [] : buffer.filter((e) => e.instanceId === id).slice(-20).map((e) => e.payload)
|
|
2043
2424
|
};
|
|
@@ -2072,6 +2453,9 @@ ${formatFixes(reports)}`);
|
|
|
2072
2453
|
bridge.flashAvoidable(!!data.arg);
|
|
2073
2454
|
reply.result = true;
|
|
2074
2455
|
break;
|
|
2456
|
+
case "explain":
|
|
2457
|
+
reply.result = bridge.explain(data.arg);
|
|
2458
|
+
break;
|
|
2075
2459
|
default:
|
|
2076
2460
|
reply.error = `unknown command ${String(data.cmd)}`;
|
|
2077
2461
|
}
|
|
@@ -2080,6 +2464,7 @@ ${formatFixes(reports)}`);
|
|
|
2080
2464
|
}
|
|
2081
2465
|
return reply;
|
|
2082
2466
|
};
|
|
2467
|
+
const isRelayGreeting = (data) => !!data && typeof data === "object" && data.__rerenderLensRelay === true && typeof data.app === "string";
|
|
2083
2468
|
const isCommand = (data) => !!data && typeof data === "object" && data.__rerenderLensCmd === true && typeof data.id === "string";
|
|
2084
2469
|
const envelope = (type, payload) => ({ [DEVTOOLS_MARKER]: true, version: PROTOCOL_VERSION, type, payload });
|
|
2085
2470
|
const sinks = [];
|
|
@@ -2102,14 +2487,20 @@ ${formatFixes(reports)}`);
|
|
|
2102
2487
|
const relayUrl = options.relay ?? (typeof window !== "undefined" ? window.__RERENDER_LENS_RELAY__ : void 0);
|
|
2103
2488
|
const ES = options.eventSource ?? (typeof EventSource === "function" ? EventSource : null);
|
|
2104
2489
|
if (relayUrl && ES && typeof fetch === "function") {
|
|
2105
|
-
const
|
|
2490
|
+
const [rawBase, relayQuery = ""] = relayUrl.split("?");
|
|
2491
|
+
const base = (rawBase ?? "").replace(/\/$/, "");
|
|
2492
|
+
const relayToken = new URLSearchParams(relayQuery).get("token");
|
|
2493
|
+
const auth = relayToken ? `token=${encodeURIComponent(relayToken)}` : "";
|
|
2106
2494
|
let queue = [];
|
|
2107
2495
|
let scheduled = false;
|
|
2496
|
+
let appId = null;
|
|
2108
2497
|
const flush = () => {
|
|
2109
2498
|
scheduled = false;
|
|
2110
2499
|
const batch = queue;
|
|
2111
2500
|
queue = [];
|
|
2112
|
-
|
|
2501
|
+
const params = [appId ? `app=${encodeURIComponent(appId)}` : "", auth].filter(Boolean).join("&");
|
|
2502
|
+
const to = `${base}/message${params ? `?${params}` : ""}`;
|
|
2503
|
+
fetch(to, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(batch), keepalive: true }).catch(() => {
|
|
2113
2504
|
});
|
|
2114
2505
|
};
|
|
2115
2506
|
const relaySend = (message) => {
|
|
@@ -2120,7 +2511,8 @@ ${formatFixes(reports)}`);
|
|
|
2120
2511
|
}
|
|
2121
2512
|
};
|
|
2122
2513
|
try {
|
|
2123
|
-
const
|
|
2514
|
+
const label = typeof location !== "undefined" ? `${location.host}${location.pathname}`.slice(0, 120) : "";
|
|
2515
|
+
const stream = new ES(`${base}/events?role=app${label ? `&label=${encodeURIComponent(label)}` : ""}${auth ? `&${auth}` : ""}`);
|
|
2124
2516
|
stream.onmessage = (event) => {
|
|
2125
2517
|
let parsed;
|
|
2126
2518
|
try {
|
|
@@ -2128,7 +2520,10 @@ ${formatFixes(reports)}`);
|
|
|
2128
2520
|
} catch {
|
|
2129
2521
|
return;
|
|
2130
2522
|
}
|
|
2131
|
-
for (const m of Array.isArray(parsed) ? parsed : [parsed])
|
|
2523
|
+
for (const m of Array.isArray(parsed) ? parsed : [parsed]) {
|
|
2524
|
+
if (isRelayGreeting(m)) appId = m.app;
|
|
2525
|
+
else if (isCommand(m)) relaySend(runCommand(m));
|
|
2526
|
+
}
|
|
2132
2527
|
};
|
|
2133
2528
|
stream.onerror = () => {
|
|
2134
2529
|
};
|