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
package/dist/vitest-setup.cjs
CHANGED
|
@@ -129,6 +129,7 @@ function deepEqualObjects(a, b, seen) {
|
|
|
129
129
|
}
|
|
130
130
|
return false;
|
|
131
131
|
}
|
|
132
|
+
var AVOIDABLE_KINDS = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
|
|
132
133
|
function classify(prev, next) {
|
|
133
134
|
if (typeof prev === "function" && typeof next === "function") {
|
|
134
135
|
return prev.name === next.name && prev.toString() === next.toString() ? "function" : "different";
|
|
@@ -165,30 +166,30 @@ function diffRecords(prev, next, basePath = "") {
|
|
|
165
166
|
if (Object.is(a, b)) continue;
|
|
166
167
|
const kind = classify(a, b);
|
|
167
168
|
if (kind === "different") {
|
|
168
|
-
changes.push({ path: diffBudgetExhausted() ? path : firstDifferentPath(a, b, path), kind, prev: a, next: b });
|
|
169
|
+
changes.push({ path: diffBudgetExhausted() ? path : firstDifferentPath(a, b, path) ?? path, kind, prev: a, next: b });
|
|
169
170
|
} else {
|
|
170
171
|
changes.push({ path, kind, prev: a, next: b });
|
|
171
172
|
}
|
|
172
173
|
}
|
|
173
174
|
return changes;
|
|
174
175
|
}
|
|
175
|
-
function firstDifferentPath(a, b, path, depth = 0) {
|
|
176
|
-
if (depth > 8) return path;
|
|
176
|
+
function firstDifferentPath(a, b, path = "", depth = 0) {
|
|
177
|
+
if (depth > 8) return path || "(value)";
|
|
177
178
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
178
|
-
if (a.length !== b.length) return `${path}.length
|
|
179
|
+
if (a.length !== b.length) return path ? `${path}.length` : "length";
|
|
179
180
|
for (let i = 0; i < a.length; i++) {
|
|
180
181
|
if (!deepEqual(a[i], b[i])) return firstDifferentPath(a[i], b[i], joinPath(path, i), depth + 1);
|
|
181
182
|
}
|
|
182
|
-
return
|
|
183
|
+
return null;
|
|
183
184
|
}
|
|
184
185
|
if (isPlainObject(a) && isPlainObject(b)) {
|
|
185
186
|
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
186
187
|
for (const k of keys) {
|
|
187
188
|
if (!deepEqual(a[k], b[k])) return firstDifferentPath(a[k], b[k], joinPath(path, k), depth + 1);
|
|
188
189
|
}
|
|
189
|
-
return
|
|
190
|
+
return null;
|
|
190
191
|
}
|
|
191
|
-
return path;
|
|
192
|
+
return deepEqual(a, b) ? null : path || "(value)";
|
|
192
193
|
}
|
|
193
194
|
|
|
194
195
|
// src/report.ts
|
|
@@ -206,10 +207,12 @@ function storeAdvice(custom) {
|
|
|
206
207
|
}
|
|
207
208
|
var isGenuine = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
|
|
208
209
|
var isChildren = (change) => change.path === "children";
|
|
210
|
+
var isRefObject = (change) => change.path === "ref" && change.kind === "deep-equal";
|
|
209
211
|
function fixFor(change) {
|
|
210
212
|
if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
|
|
211
213
|
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`;
|
|
212
214
|
}
|
|
215
|
+
if (isRefObject(change)) return `create the ref once with useRef (or createRef outside the render) and pass the same object every time`;
|
|
213
216
|
switch (change.kind) {
|
|
214
217
|
case "function":
|
|
215
218
|
return `wrap it in useCallback (or hoist it out of the parent's render)`;
|
|
@@ -225,6 +228,7 @@ function describe(change) {
|
|
|
225
228
|
if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
|
|
226
229
|
return `children are new React elements with the same types and props on every render of the parent`;
|
|
227
230
|
}
|
|
231
|
+
if (isRefObject(change)) return `prop "ref" is a new ref object on every render (React re-attaches it each time)`;
|
|
228
232
|
switch (change.kind) {
|
|
229
233
|
case "deep-equal":
|
|
230
234
|
return `prop "${change.path}" is a new reference but deep-equal to the previous value`;
|
|
@@ -252,11 +256,21 @@ function buildReport(input) {
|
|
|
252
256
|
if (causes.length === 0) trigger = "parent";
|
|
253
257
|
else if (causes.length === 1) trigger = causes[0];
|
|
254
258
|
else trigger = "mixed";
|
|
255
|
-
const
|
|
259
|
+
const nothingChanged = input.propChanges.length === 0 && stateChanges.length === 0 && hookChanges.length === 0;
|
|
260
|
+
const cacheHit = input.compiled === true && trigger === "parent" && input.propChanges.length === 0;
|
|
261
|
+
const revealed = input.revealed === true && trigger === "parent";
|
|
262
|
+
const avoidable = trigger === "parent" && !cacheHit && !revealed;
|
|
256
263
|
const reasons = [];
|
|
257
|
-
|
|
264
|
+
const memoFix = input.classComponent ? `Make "${input.component}" extend PureComponent (or implement shouldComponentUpdate)` : `Wrap "${input.component}" in React.memo (or extend PureComponent)`;
|
|
265
|
+
if (cacheHit) {
|
|
266
|
+
reasons.push(`compiled by React Compiler: its output is memoized and the inputs did not change, so this render is cheap; nothing to fix.`);
|
|
267
|
+
} else if (revealed) {
|
|
268
|
+
reasons.push(
|
|
269
|
+
`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.`
|
|
270
|
+
);
|
|
271
|
+
} else if (avoidable && nothingChanged) {
|
|
258
272
|
const who = input.parent ? `<${input.parent.name}> re-rendered (${describeTrigger(input.parent.trigger)})` : "its parent re-rendered";
|
|
259
|
-
reasons.push(`re-rendered with identical props because ${who}.
|
|
273
|
+
reasons.push(`re-rendered with identical props because ${who}. ${memoFix}.`);
|
|
260
274
|
} else if (avoidable && input.parent) {
|
|
261
275
|
reasons.push(`caused by <${input.parent.name}> re-rendering (${describeTrigger(input.parent.trigger)}).`);
|
|
262
276
|
}
|
|
@@ -265,9 +279,9 @@ function buildReport(input) {
|
|
|
265
279
|
reasons.push(fix ? `${describe(c)}: ${fix}.` : `${describe(c)}.`);
|
|
266
280
|
}
|
|
267
281
|
const memoized = input.memoized !== false;
|
|
268
|
-
if (avoidable && !memoized && input.propChanges.length > 0) {
|
|
282
|
+
if (avoidable && !memoized && !input.compiled && input.propChanges.length > 0) {
|
|
269
283
|
reasons.push(
|
|
270
|
-
`"${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).`
|
|
284
|
+
`"${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)"}.`
|
|
271
285
|
);
|
|
272
286
|
}
|
|
273
287
|
for (const c of stateChanges) {
|
|
@@ -279,6 +293,8 @@ function buildReport(input) {
|
|
|
279
293
|
const where = c.provider && c.provider.component ? ` (provided by <${c.provider.component}>)` : "";
|
|
280
294
|
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` : "";
|
|
281
295
|
reasons.push(`${c.path} changed${where}${keys}.`);
|
|
296
|
+
} else if (c.hook === "useDeferredValue" && isGenuine(c)) {
|
|
297
|
+
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.`);
|
|
282
298
|
} else if (isGenuine(c)) reasons.push(`${hookLabel(c)} changed.`);
|
|
283
299
|
else if (isStateHook(c))
|
|
284
300
|
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.`);
|
|
@@ -309,6 +325,7 @@ function buildReport(input) {
|
|
|
309
325
|
reasons,
|
|
310
326
|
time: now()
|
|
311
327
|
};
|
|
328
|
+
if (input.compiled) report.compiled = true;
|
|
312
329
|
if (input.selfDuration !== void 0) report.selfDuration = input.selfDuration;
|
|
313
330
|
if (input.treeDuration !== void 0) report.treeDuration = input.treeDuration;
|
|
314
331
|
if (input.commitPriority) report.commitPriority = input.commitPriority;
|
|
@@ -346,10 +363,10 @@ var KIND_LABEL = {
|
|
|
346
363
|
};
|
|
347
364
|
function summarize(report) {
|
|
348
365
|
const counts2 = /* @__PURE__ */ new Map();
|
|
349
|
-
for (const c of [...report.propChanges, ...report.stateChanges, ...report.hookChanges]) {
|
|
366
|
+
for (const c of [...report.propChanges || [], ...report.stateChanges || [], ...report.hookChanges || []]) {
|
|
350
367
|
counts2.set(c.kind, (counts2.get(c.kind) ?? 0) + 1);
|
|
351
368
|
}
|
|
352
|
-
const parts = [...counts2].map(([kind, n]) => `${n} ${KIND_LABEL[kind]}`);
|
|
369
|
+
const parts = [...counts2].map(([kind, n]) => `${n} ${KIND_LABEL[kind] || kind}`);
|
|
353
370
|
if (parts.length === 0) parts.push("no changes");
|
|
354
371
|
return parts.join(", ");
|
|
355
372
|
}
|
|
@@ -371,16 +388,27 @@ function printReport(report, options) {
|
|
|
371
388
|
}
|
|
372
389
|
|
|
373
390
|
// src/state.ts
|
|
391
|
+
var MAX_SEEN_COMPONENTS = 500;
|
|
374
392
|
var KEY = /* @__PURE__ */ Symbol.for("rerender-lens.state");
|
|
375
393
|
function getState() {
|
|
376
394
|
const g = globalThis;
|
|
377
395
|
let s = g[KEY];
|
|
378
396
|
if (!s) {
|
|
379
|
-
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 };
|
|
397
|
+
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 };
|
|
380
398
|
g[KEY] = s;
|
|
381
399
|
}
|
|
382
400
|
return s;
|
|
383
401
|
}
|
|
402
|
+
function noteRendered(s, name, tracked) {
|
|
403
|
+
if (!s.seen.has(name)) {
|
|
404
|
+
if (s.seen.size >= MAX_SEEN_COMPONENTS) {
|
|
405
|
+
s.seenOverflow = true;
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
s.seen.add(name);
|
|
409
|
+
}
|
|
410
|
+
if (tracked) s.seenTracked.add(name);
|
|
411
|
+
}
|
|
384
412
|
function warnOnce(key, message) {
|
|
385
413
|
const s = getState();
|
|
386
414
|
if (s.warnedOnce.has(key)) return;
|
|
@@ -458,6 +486,9 @@ function init(options = {}) {
|
|
|
458
486
|
const s = getState();
|
|
459
487
|
s.options = { ...options };
|
|
460
488
|
s.printed.clear();
|
|
489
|
+
s.seen.clear();
|
|
490
|
+
s.seenTracked.clear();
|
|
491
|
+
s.seenOverflow = false;
|
|
461
492
|
if (!s.enabled) {
|
|
462
493
|
s.detach = attach();
|
|
463
494
|
s.enabled = true;
|
|
@@ -473,6 +504,27 @@ function disable() {
|
|
|
473
504
|
}
|
|
474
505
|
|
|
475
506
|
// src/hookNames.ts
|
|
507
|
+
var NODE_COUNT = {
|
|
508
|
+
useState: 1,
|
|
509
|
+
useReducer: 1,
|
|
510
|
+
useRef: 1,
|
|
511
|
+
useMemo: 1,
|
|
512
|
+
useCallback: 1,
|
|
513
|
+
useEffect: 1,
|
|
514
|
+
useLayoutEffect: 1,
|
|
515
|
+
useInsertionEffect: 1,
|
|
516
|
+
useImperativeHandle: 1,
|
|
517
|
+
useSyncExternalStore: 2,
|
|
518
|
+
useTransition: 2,
|
|
519
|
+
useDeferredValue: 1,
|
|
520
|
+
useId: 1,
|
|
521
|
+
useOptimistic: 1,
|
|
522
|
+
useActionState: 3,
|
|
523
|
+
useFormState: 3,
|
|
524
|
+
useEffectEvent: 1,
|
|
525
|
+
useCacheRefresh: 1
|
|
526
|
+
};
|
|
527
|
+
var BUILTIN_HOOKS = /* @__PURE__ */ new Set([...Object.keys(NODE_COUNT), "useContext", "useDebugValue", "use"]);
|
|
476
528
|
var noop = () => {
|
|
477
529
|
};
|
|
478
530
|
function customHooksFromStack(stack, componentName, marker) {
|
|
@@ -487,7 +539,7 @@ function customHooksFromStack(stack, componentName, marker) {
|
|
|
487
539
|
continue;
|
|
488
540
|
}
|
|
489
541
|
if (name === componentName || name.endsWith("." + componentName)) break;
|
|
490
|
-
if (/^use[A-Z0-9_]/.test(name)) names.push(name);
|
|
542
|
+
if (/^use[A-Z0-9_]/.test(name) && !BUILTIN_HOOKS.has(name)) names.push(name);
|
|
491
543
|
if (names.length > 8) break;
|
|
492
544
|
}
|
|
493
545
|
return names;
|
|
@@ -771,6 +823,11 @@ function isMemoizedFiber(fiber) {
|
|
|
771
823
|
}
|
|
772
824
|
return false;
|
|
773
825
|
}
|
|
826
|
+
function isCompiledFiber(fiber) {
|
|
827
|
+
if (fiber.tag === ClassComponent) return false;
|
|
828
|
+
const cache2 = fiber.updateQueue?.memoCache;
|
|
829
|
+
return !!cache2 && Array.isArray(cache2.data);
|
|
830
|
+
}
|
|
774
831
|
function durationsOf(fiber) {
|
|
775
832
|
if (typeof fiber.actualDuration !== "number") return null;
|
|
776
833
|
const tree = fiber.actualDuration;
|
|
@@ -923,7 +980,8 @@ function diffHooks(fiber, alt) {
|
|
|
923
980
|
const labels = hookLabelsFor(fiber, b);
|
|
924
981
|
let i = 0;
|
|
925
982
|
while (a && b) {
|
|
926
|
-
|
|
983
|
+
const deferred = labels?.[i] === "useDeferredValue";
|
|
984
|
+
if ((deferred || isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
|
|
927
985
|
const hook = labels?.[i] ?? hookLabel2(b);
|
|
928
986
|
out.push({
|
|
929
987
|
path: `${hook}#${i}`,
|
|
@@ -957,6 +1015,10 @@ function diffContexts(fiber, alt) {
|
|
|
957
1015
|
let a = alt.dependencies?.firstContext ?? null;
|
|
958
1016
|
let b = fiber.dependencies?.firstContext ?? null;
|
|
959
1017
|
let i = 0;
|
|
1018
|
+
if (b && !("memoizedValue" in b)) {
|
|
1019
|
+
warnOnce("contextValues", "this React does not record context values on the fiber (React < 18): context changes are reported as plain parent re-renders.");
|
|
1020
|
+
return out;
|
|
1021
|
+
}
|
|
960
1022
|
while (a && b) {
|
|
961
1023
|
if (a.context === b.context && !Object.is(a.memoizedValue, b.memoizedValue)) {
|
|
962
1024
|
const name = b.context.displayName ?? "Context";
|
|
@@ -989,10 +1051,26 @@ function diffContexts(fiber, alt) {
|
|
|
989
1051
|
return out;
|
|
990
1052
|
}
|
|
991
1053
|
var isGenuine2 = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
|
|
1054
|
+
var isRefObject2 = (v) => {
|
|
1055
|
+
if (typeof v !== "object" || v === null) return false;
|
|
1056
|
+
const keys = Object.keys(v);
|
|
1057
|
+
return keys.length === 1 && keys[0] === "current";
|
|
1058
|
+
};
|
|
1059
|
+
function diffProps(prev, next) {
|
|
1060
|
+
if (prev === next) return [];
|
|
1061
|
+
const pr = prev.ref;
|
|
1062
|
+
const nr = next.ref;
|
|
1063
|
+
if (pr !== nr && isRefObject2(pr) && isRefObject2(nr)) {
|
|
1064
|
+
const { ref: _p, ...restPrev } = prev;
|
|
1065
|
+
const { ref: _n, ...restNext } = next;
|
|
1066
|
+
return [{ path: "ref", kind: "deep-equal", prev: pr, next: nr }, ...diffRecords(restPrev, restNext)];
|
|
1067
|
+
}
|
|
1068
|
+
return diffRecords(prev, next);
|
|
1069
|
+
}
|
|
992
1070
|
function analyze(fiber, alt, trackHooks) {
|
|
993
1071
|
const prevProps = alt.memoizedProps ?? {};
|
|
994
1072
|
const nextProps = fiber.memoizedProps ?? {};
|
|
995
|
-
const propChanges =
|
|
1073
|
+
const propChanges = diffProps(prevProps, nextProps);
|
|
996
1074
|
let stateChanges = [];
|
|
997
1075
|
let hookChanges = [];
|
|
998
1076
|
if (fiber.tag === ClassComponent) {
|
|
@@ -1005,7 +1083,7 @@ function analyze(fiber, alt, trackHooks) {
|
|
|
1005
1083
|
} else if (trackHooks) {
|
|
1006
1084
|
hookChanges = [...diffHooks(fiber, alt), ...diffContexts(fiber, alt)];
|
|
1007
1085
|
}
|
|
1008
|
-
const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore";
|
|
1086
|
+
const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore" && h.hook !== "useDeferredValue";
|
|
1009
1087
|
const causes = [];
|
|
1010
1088
|
if (propChanges.some(isGenuine2)) causes.push("props");
|
|
1011
1089
|
if (stateChanges.some(isGenuine2) || hookChanges.some((h) => isStateHook(h) && isGenuine2(h))) causes.push("state");
|
|
@@ -1045,6 +1123,24 @@ var lastCommit = null;
|
|
|
1045
1123
|
var EFFECT_LOOP_WINDOW_MS = 50;
|
|
1046
1124
|
var MAX_REPORTS_PER_COMMIT = 200;
|
|
1047
1125
|
var COMMIT_TIME_BUDGET_MS = 25;
|
|
1126
|
+
function isDeferredCatchUp(fiber) {
|
|
1127
|
+
const types = fiber._debugHookTypes;
|
|
1128
|
+
if (!types || !types.includes("useDeferredValue") || !fiber.alternate) return false;
|
|
1129
|
+
const first = fiber.memoizedState;
|
|
1130
|
+
if (!isHookList(first)) return false;
|
|
1131
|
+
const labels = hookLabelsFor(fiber, first);
|
|
1132
|
+
if (!labels) return false;
|
|
1133
|
+
let a = fiber.alternate.memoizedState;
|
|
1134
|
+
let b = first;
|
|
1135
|
+
for (let i = 0; a && b; a = a.next, b = b.next, i++) {
|
|
1136
|
+
if (labels[i] === "useDeferredValue" && !Object.is(a.memoizedState, b.memoizedState)) return true;
|
|
1137
|
+
}
|
|
1138
|
+
return false;
|
|
1139
|
+
}
|
|
1140
|
+
function isUnder(fiber, boundaries) {
|
|
1141
|
+
for (let f = fiber.return; f && f.tag !== HostRoot; f = f.return) if (f.tag === SuspenseComponent && boundaries.includes(f)) return true;
|
|
1142
|
+
return false;
|
|
1143
|
+
}
|
|
1048
1144
|
function onCommit(root, commitPriority) {
|
|
1049
1145
|
const s = getState();
|
|
1050
1146
|
if (!s.enabled) return;
|
|
@@ -1054,17 +1150,20 @@ function onCommit(root, commitPriority) {
|
|
|
1054
1150
|
const rendered = [];
|
|
1055
1151
|
const renderedNames = /* @__PURE__ */ new Set();
|
|
1056
1152
|
let hot = false;
|
|
1057
|
-
let
|
|
1153
|
+
let resolvedBoundaries = null;
|
|
1058
1154
|
const stack = [root.current];
|
|
1059
1155
|
while (stack.length) {
|
|
1060
1156
|
const fiber = stack.pop();
|
|
1061
1157
|
const alt = fiber.alternate;
|
|
1062
1158
|
if (alt && isComponentTag(fiber.tag) && didRender(fiber)) {
|
|
1063
1159
|
if (isHotSwapped(fiber, alt)) hot = true;
|
|
1064
|
-
|
|
1065
|
-
|
|
1160
|
+
const name = fiberName(fiber);
|
|
1161
|
+
renderedNames.add(name);
|
|
1162
|
+
const tracked = shouldTrack(fiberType(fiber), o);
|
|
1163
|
+
noteRendered(s, name, tracked);
|
|
1164
|
+
if (tracked) rendered.push(fiber);
|
|
1066
1165
|
}
|
|
1067
|
-
if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null)
|
|
1166
|
+
if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) (resolvedBoundaries ?? (resolvedBoundaries = [])).push(fiber);
|
|
1068
1167
|
if (!alt || fiber.child !== alt.child) {
|
|
1069
1168
|
for (let c = fiber.child; c; c = c.sibling) stack.push(c);
|
|
1070
1169
|
}
|
|
@@ -1076,10 +1175,10 @@ function onCommit(root, commitPriority) {
|
|
|
1076
1175
|
let commitCause;
|
|
1077
1176
|
let afterCommit;
|
|
1078
1177
|
const inputDriven = commitPriority === "immediate" || commitPriority === "user-blocking";
|
|
1079
|
-
if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u))) {
|
|
1178
|
+
if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u)) && !rendered.some(isDeferredCatchUp)) {
|
|
1080
1179
|
commitCause = "effect-after-commit";
|
|
1081
1180
|
afterCommit = lastCommit.id;
|
|
1082
|
-
} else if (
|
|
1181
|
+
} else if (resolvedBoundaries) commitCause = "suspense-resolved";
|
|
1083
1182
|
if (rendered.length === 0) {
|
|
1084
1183
|
lastCommit = { id: s.nextCommitId, at, rendered: renderedNames };
|
|
1085
1184
|
return;
|
|
@@ -1131,6 +1230,9 @@ function onCommit(root, commitPriority) {
|
|
|
1131
1230
|
owner: ownerName(fiber),
|
|
1132
1231
|
path: componentPath(fiber),
|
|
1133
1232
|
memoized: isMemoizedFiber(fiber),
|
|
1233
|
+
compiled: isCompiledFiber(fiber),
|
|
1234
|
+
classComponent: fiber.tag === ClassComponent,
|
|
1235
|
+
revealed: resolvedBoundaries !== null && a.trigger === "parent" && isUnder(fiber, resolvedBoundaries),
|
|
1134
1236
|
selfDuration: durations ? durations.self : void 0,
|
|
1135
1237
|
treeDuration: durations ? durations.tree : void 0,
|
|
1136
1238
|
commitId,
|
|
@@ -1144,38 +1246,203 @@ function onCommit(root, commitPriority) {
|
|
|
1144
1246
|
}
|
|
1145
1247
|
}
|
|
1146
1248
|
|
|
1249
|
+
// src/causes.ts
|
|
1250
|
+
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);
|
|
1251
|
+
var bump = (m, key, n = 1) => void m.set(key, (m.get(key) || 0) + n);
|
|
1252
|
+
function indexByComponent(reports) {
|
|
1253
|
+
const index = /* @__PURE__ */ new Map();
|
|
1254
|
+
for (const r of reports) {
|
|
1255
|
+
const list = index.get(r.component);
|
|
1256
|
+
if (list) list.push(r);
|
|
1257
|
+
else index.set(r.component, [r]);
|
|
1258
|
+
}
|
|
1259
|
+
return index;
|
|
1260
|
+
}
|
|
1261
|
+
function rootCauseOf(r, commitReports, index = indexByComponent(commitReports)) {
|
|
1262
|
+
let cur = r;
|
|
1263
|
+
const seen = /* @__PURE__ */ new Set([r]);
|
|
1264
|
+
while (cur.trigger === "parent" && cur.parent) {
|
|
1265
|
+
const parent = cur.parent;
|
|
1266
|
+
const candidates = index.get(parent.name);
|
|
1267
|
+
const p = candidates && candidates.find((x) => isAncestorReport(x, cur));
|
|
1268
|
+
if (!p || seen.has(p)) return { name: parent.name, trigger: parent.trigger, report: null };
|
|
1269
|
+
seen.add(p);
|
|
1270
|
+
cur = p;
|
|
1271
|
+
}
|
|
1272
|
+
return cur === r ? null : { name: cur.component, trigger: cur.trigger, report: cur };
|
|
1273
|
+
}
|
|
1274
|
+
function rootCausesOf(reports) {
|
|
1275
|
+
const roots = /* @__PURE__ */ new Map();
|
|
1276
|
+
const rootByReport = /* @__PURE__ */ new Map();
|
|
1277
|
+
const index = indexByComponent(reports);
|
|
1278
|
+
let avoidable = 0;
|
|
1279
|
+
let wasted = 0;
|
|
1280
|
+
for (const r of reports) {
|
|
1281
|
+
if (!r.avoidable) continue;
|
|
1282
|
+
avoidable++;
|
|
1283
|
+
if (typeof r.selfDuration === "number") wasted += r.selfDuration;
|
|
1284
|
+
const root = rootCauseOf(r, reports, index);
|
|
1285
|
+
const name = root ? root.name : r.parent && r.parent.name || "(unknown)";
|
|
1286
|
+
const trigger = root ? root.trigger : r.parent && r.parent.trigger || "parent";
|
|
1287
|
+
rootByReport.set(r, name);
|
|
1288
|
+
let agg = roots.get(name);
|
|
1289
|
+
if (!agg) {
|
|
1290
|
+
agg = { name, trigger, count: 0, components: /* @__PURE__ */ new Map() };
|
|
1291
|
+
roots.set(name, agg);
|
|
1292
|
+
}
|
|
1293
|
+
agg.count++;
|
|
1294
|
+
bump(agg.components, r.component);
|
|
1295
|
+
}
|
|
1296
|
+
return { roots: [...roots.values()].sort((a, b) => b.count - a.count), rootByReport, avoidable, wasted };
|
|
1297
|
+
}
|
|
1298
|
+
function groupByCommit(reports) {
|
|
1299
|
+
const out = /* @__PURE__ */ new Map();
|
|
1300
|
+
for (const r of reports) {
|
|
1301
|
+
if (typeof r.commitId !== "number" || r.commitId <= 0) continue;
|
|
1302
|
+
const list = out.get(r.commitId);
|
|
1303
|
+
if (list) list.push(r);
|
|
1304
|
+
else out.set(r.commitId, [r]);
|
|
1305
|
+
}
|
|
1306
|
+
return out;
|
|
1307
|
+
}
|
|
1308
|
+
function rankRootCauses(reports) {
|
|
1309
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1310
|
+
for (const commit of groupByCommit(reports).values()) {
|
|
1311
|
+
for (const root of rootCausesOf(commit).roots) {
|
|
1312
|
+
let agg = byName.get(root.name);
|
|
1313
|
+
if (!agg) {
|
|
1314
|
+
agg = { name: root.name, trigger: root.trigger, commits: 0, count: 0, components: /* @__PURE__ */ new Map() };
|
|
1315
|
+
byName.set(root.name, agg);
|
|
1316
|
+
}
|
|
1317
|
+
agg.commits++;
|
|
1318
|
+
agg.count += root.count;
|
|
1319
|
+
for (const [c, n] of root.components) bump(agg.components, c, n);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
return [...byName.values()].sort((a, b) => b.count - a.count || b.commits - a.commits || a.name.localeCompare(b.name));
|
|
1323
|
+
}
|
|
1324
|
+
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
1325
|
+
function formatRootCauses(reports, limit = 5) {
|
|
1326
|
+
const ranked = rankRootCauses(reports);
|
|
1327
|
+
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(", ")})`);
|
|
1328
|
+
if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
|
|
1329
|
+
return lines.join("\n");
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1147
1332
|
// src/fixes.ts
|
|
1148
|
-
var
|
|
1333
|
+
var MAX_FIX_REPORTS = 50;
|
|
1149
1334
|
var rootOf = (path) => path.split(/[.[]/)[0] || path;
|
|
1335
|
+
var identifier = (name) => /^[A-Za-z_$][\w$]*$/.test(name) ? name : "value";
|
|
1336
|
+
function shortValue(v, max = 60) {
|
|
1337
|
+
let s;
|
|
1338
|
+
try {
|
|
1339
|
+
s = JSON.stringify(v);
|
|
1340
|
+
} catch {
|
|
1341
|
+
s = String(v);
|
|
1342
|
+
}
|
|
1343
|
+
if (s === void 0) s = String(v);
|
|
1344
|
+
return s.length > max ? s.slice(0, max - 3) + "..." : s;
|
|
1345
|
+
}
|
|
1150
1346
|
function fixesFor(r) {
|
|
1151
1347
|
const out = [];
|
|
1152
|
-
const
|
|
1153
|
-
const
|
|
1348
|
+
const ownerName2 = r.owner || r.parent && r.parent.name || null;
|
|
1349
|
+
const propChanges = r.propChanges || [];
|
|
1350
|
+
const changes = [...propChanges, ...r.stateChanges || [], ...r.hookChanges || []];
|
|
1154
1351
|
if (r.avoidable && (changes.length === 0 || r.memoized === false)) {
|
|
1352
|
+
const identical = changes.length === 0;
|
|
1155
1353
|
out.push({
|
|
1156
1354
|
kind: "memo",
|
|
1157
1355
|
owner: r.component,
|
|
1158
1356
|
target: r.component,
|
|
1159
1357
|
prop: null,
|
|
1160
1358
|
label: `Wrap <${r.component}> in React.memo`,
|
|
1161
|
-
detail:
|
|
1359
|
+
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.`,
|
|
1360
|
+
snippet: `// ${r.component}
|
|
1361
|
+
import { memo } from 'react';
|
|
1362
|
+
|
|
1363
|
+
export const ${r.component} = memo(function ${r.component}(props) {
|
|
1364
|
+
// ...
|
|
1365
|
+
});
|
|
1366
|
+
// class components: extend PureComponent instead`
|
|
1162
1367
|
});
|
|
1163
1368
|
}
|
|
1164
|
-
for (const c of
|
|
1165
|
-
if (!
|
|
1369
|
+
for (const c of propChanges) {
|
|
1370
|
+
if (!AVOIDABLE_KINDS.has(c.kind)) continue;
|
|
1371
|
+
const owner = ownerName2 || "?";
|
|
1166
1372
|
const root = rootOf(c.path);
|
|
1373
|
+
const id = identifier(root);
|
|
1167
1374
|
if (root === "children" && (c.kind === "element" || c.kind === "deep-equal")) {
|
|
1168
|
-
out.push({
|
|
1375
|
+
out.push({
|
|
1376
|
+
kind: "children",
|
|
1377
|
+
owner,
|
|
1378
|
+
target: r.component,
|
|
1379
|
+
prop: "children",
|
|
1380
|
+
label: `memoize children of <${r.component}> in <${owner}>`,
|
|
1381
|
+
detail: `<${owner}> re-creates the children of <${r.component}> on every render; they have the same types and props each time.`,
|
|
1382
|
+
snippet: `// ${owner}
|
|
1383
|
+
import { useMemo } from 'react';
|
|
1384
|
+
|
|
1385
|
+
const children = useMemo(() => (
|
|
1386
|
+
<>{/* the same elements */}</>
|
|
1387
|
+
), [/* deps */]);
|
|
1388
|
+
|
|
1389
|
+
<${r.component}>{children}</${r.component}>
|
|
1390
|
+
|
|
1391
|
+
// static children: hoist them to module scope
|
|
1392
|
+
const STATIC = <em>hi</em>;`
|
|
1393
|
+
});
|
|
1169
1394
|
} else if (c.kind === "function") {
|
|
1170
|
-
out.push({
|
|
1395
|
+
out.push({
|
|
1396
|
+
kind: "useCallback",
|
|
1397
|
+
owner,
|
|
1398
|
+
target: r.component,
|
|
1399
|
+
prop: root,
|
|
1400
|
+
label: `useCallback(${root}) in <${owner}>`,
|
|
1401
|
+
detail: `prop "${c.path}" of <${r.component}> is a new function on every render of <${owner}>.`,
|
|
1402
|
+
snippet: `// ${owner}
|
|
1403
|
+
import { useCallback } from 'react';
|
|
1404
|
+
|
|
1405
|
+
const ${id} = useCallback((/* args */) => {
|
|
1406
|
+
// ...
|
|
1407
|
+
}, [/* deps */]);
|
|
1408
|
+
|
|
1409
|
+
<${r.component} ${root}={${id}} />`
|
|
1410
|
+
});
|
|
1171
1411
|
} else if (c.kind === "element") {
|
|
1172
|
-
out.push({
|
|
1412
|
+
out.push({
|
|
1413
|
+
kind: "useMemoElement",
|
|
1414
|
+
owner,
|
|
1415
|
+
target: r.component,
|
|
1416
|
+
prop: root,
|
|
1417
|
+
label: `memoize element prop ${root} in <${owner}>`,
|
|
1418
|
+
detail: `prop "${c.path}" of <${r.component}> is a new element with the same type and props on every render of <${owner}>.`,
|
|
1419
|
+
snippet: `// ${owner}
|
|
1420
|
+
import { useMemo } from 'react';
|
|
1421
|
+
|
|
1422
|
+
const ${id} = useMemo(() => ${shortValue(c.next, 40)}, [/* deps */]);
|
|
1423
|
+
// or pass it as children from a component that does not re-render`
|
|
1424
|
+
});
|
|
1173
1425
|
} else {
|
|
1174
|
-
|
|
1426
|
+
const isArray = Array.isArray(c.next);
|
|
1427
|
+
out.push({
|
|
1428
|
+
kind: "useMemo",
|
|
1429
|
+
owner,
|
|
1430
|
+
target: r.component,
|
|
1431
|
+
prop: root,
|
|
1432
|
+
label: `useMemo(${root}) in <${owner}>`,
|
|
1433
|
+
detail: `prop "${c.path}" of <${r.component}> is a new ${isArray ? "array" : "object"} with the same contents on every render of <${owner}>.`,
|
|
1434
|
+
snippet: `// ${owner}
|
|
1435
|
+
import { useMemo } from 'react';
|
|
1436
|
+
|
|
1437
|
+
const ${id} = useMemo(() => (${shortValue(c.next, 80)}), [/* deps */]);
|
|
1438
|
+
|
|
1439
|
+
// or, when it never changes, hoist it to module scope:
|
|
1440
|
+
const ${id.toUpperCase()} = ${shortValue(c.next, 80)};`
|
|
1441
|
+
});
|
|
1175
1442
|
}
|
|
1176
1443
|
}
|
|
1177
|
-
for (const c of [...r.stateChanges, ...r.hookChanges]) {
|
|
1178
|
-
const isContext = c.hook === "useContext";
|
|
1444
|
+
for (const c of [...r.stateChanges || [], ...r.hookChanges || []]) {
|
|
1445
|
+
const isContext = c.hook === "useContext" || /^useContext/.test(c.path);
|
|
1179
1446
|
const ctxName = isContext ? (/useContext\((.*)\)/.exec(c.path) || [])[1] || "Context" : "";
|
|
1180
1447
|
const providerOwner = isContext && c.provider && c.provider.component ? c.provider.component : null;
|
|
1181
1448
|
if (isContext && c.kind === "different" && c.changedKeys && typeof c.totalKeys === "number" && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys) {
|
|
@@ -1185,20 +1452,63 @@ function fixesFor(r) {
|
|
|
1185
1452
|
target: r.component,
|
|
1186
1453
|
prop: ctxName,
|
|
1187
1454
|
label: `split ${ctxName}${providerOwner ? ` in <${providerOwner}>` : ""}: only ${c.changedKeys.join(", ")} changed`,
|
|
1188
|
-
detail: `${c.changedKeys.
|
|
1455
|
+
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.`,
|
|
1456
|
+
snippet: `// ${providerOwner || "Provider"}
|
|
1457
|
+
// one context per independently-changing slice
|
|
1458
|
+
const ${identifier(ctxName)}Static = createContext(...);
|
|
1459
|
+
const ${identifier(ctxName)}${c.changedKeys.map((k) => k[0].toUpperCase() + k.slice(1)).join("")} = createContext(...);
|
|
1460
|
+
|
|
1461
|
+
// or keep one context and let consumers select a slice:
|
|
1462
|
+
const ${c.changedKeys[0]} = useContextSelector(${ctxName}, (v) => v.${c.changedKeys[0]});`
|
|
1189
1463
|
});
|
|
1190
1464
|
continue;
|
|
1191
1465
|
}
|
|
1192
|
-
if (!
|
|
1466
|
+
if (!AVOIDABLE_KINDS.has(c.kind)) continue;
|
|
1193
1467
|
if (isContext) {
|
|
1194
|
-
out.push({
|
|
1468
|
+
out.push({
|
|
1469
|
+
kind: "contextValue",
|
|
1470
|
+
owner: providerOwner || `${ctxName}.Provider`,
|
|
1471
|
+
target: r.component,
|
|
1472
|
+
prop: ctxName,
|
|
1473
|
+
label: `memoize the ${ctxName} provider value${providerOwner ? ` in <${providerOwner}>` : ""}`,
|
|
1474
|
+
detail: `<${r.component}> re-rendered because ${ctxName} produced a new value that is deep-equal to the previous one${providerOwner ? ` (Provider rendered by <${providerOwner}>)` : ""}.`,
|
|
1475
|
+
snippet: `// ${providerOwner || `where <${ctxName}.Provider> is rendered`}
|
|
1476
|
+
const value = useMemo(() => ({ /* ... */ }), [/* deps */]);
|
|
1477
|
+
<${ctxName}.Provider value={value}>`
|
|
1478
|
+
});
|
|
1195
1479
|
} else if (c.hook === "useSyncExternalStore") {
|
|
1196
1480
|
const chain = c.custom || [];
|
|
1197
1481
|
const redux = chain.some((n) => /^use(App)?Selector$/.test(n));
|
|
1198
1482
|
const zustand = !redux && chain.some((n) => /^use[A-Z]\w*Store$/.test(n) || n === "useStore" || n === "useBoundStore");
|
|
1199
|
-
|
|
1483
|
+
const via = chain.length ? ` via ${chain.join(" \u203A ")}` : "";
|
|
1484
|
+
out.push({
|
|
1485
|
+
kind: "storeSnapshot",
|
|
1486
|
+
owner: r.component,
|
|
1487
|
+
target: r.component,
|
|
1488
|
+
prop: c.path,
|
|
1489
|
+
label: redux ? `memoize the selector in <${r.component}>` : zustand ? `useShallow in <${r.component}>` : `stable getSnapshot in <${r.component}>`,
|
|
1490
|
+
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.`,
|
|
1491
|
+
snippet: redux ? `// ${r.component}
|
|
1492
|
+
import { shallowEqual } from 'react-redux';
|
|
1493
|
+
const slice = useSelector(selectSlice, shallowEqual);
|
|
1494
|
+
// or memoize: const selectSlice = createSelector([selectA, selectB], (a, b) => ({ a, b }));` : zustand ? `// ${r.component}
|
|
1495
|
+
import { useShallow } from 'zustand/react/shallow';
|
|
1496
|
+
const { a, b } = useStore(useShallow((s) => ({ a: s.a, b: s.b })));
|
|
1497
|
+
// or select a primitive: const a = useStore((s) => s.a);` : `// ${r.component}
|
|
1498
|
+
// getSnapshot must return the same reference while the data is unchanged
|
|
1499
|
+
const snapshot = useSyncExternalStore(subscribe, store.getSnapshot /* cached */);`
|
|
1500
|
+
});
|
|
1200
1501
|
} else {
|
|
1201
|
-
out.push({
|
|
1502
|
+
out.push({
|
|
1503
|
+
kind: "bailout",
|
|
1504
|
+
owner: r.component,
|
|
1505
|
+
target: r.component,
|
|
1506
|
+
prop: c.path,
|
|
1507
|
+
label: `bail out before setting ${c.path} in <${r.component}>`,
|
|
1508
|
+
detail: `${c.path} was set to a value deep-equal to the current one (new reference).`,
|
|
1509
|
+
snippet: `// ${r.component}
|
|
1510
|
+
setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
1511
|
+
});
|
|
1202
1512
|
}
|
|
1203
1513
|
}
|
|
1204
1514
|
return out;
|
|
@@ -1212,11 +1522,12 @@ function rankFixes(reports) {
|
|
|
1212
1522
|
const key = fixKey(f);
|
|
1213
1523
|
let agg = byKey.get(key);
|
|
1214
1524
|
if (!agg) {
|
|
1215
|
-
agg = { ...f, key, count: 0, components: {} };
|
|
1525
|
+
agg = { ...f, key, count: 0, components: {}, reports: [] };
|
|
1216
1526
|
byKey.set(key, agg);
|
|
1217
1527
|
}
|
|
1218
1528
|
agg.count++;
|
|
1219
1529
|
agg.components[r.component] = (agg.components[r.component] || 0) + 1;
|
|
1530
|
+
if (agg.reports.length < MAX_FIX_REPORTS) agg.reports.push(r);
|
|
1220
1531
|
}
|
|
1221
1532
|
}
|
|
1222
1533
|
return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
|
|
@@ -1226,6 +1537,8 @@ function formatFixes(reports, limit = 10) {
|
|
|
1226
1537
|
if (!ranked.length) return "No avoidable re-renders.";
|
|
1227
1538
|
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(", ")})`);
|
|
1228
1539
|
if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
|
|
1540
|
+
const roots = formatRootCauses(reports, 5);
|
|
1541
|
+
if (roots) lines.push("", "Root causes:", roots);
|
|
1229
1542
|
return lines.join("\n");
|
|
1230
1543
|
}
|
|
1231
1544
|
|
|
@@ -1324,7 +1637,7 @@ ${formatFixes(reports)}`);
|
|
|
1324
1637
|
};
|
|
1325
1638
|
}
|
|
1326
1639
|
|
|
1327
|
-
// src/
|
|
1640
|
+
// src/runner-report.ts
|
|
1328
1641
|
var OUT_ENV = "RERENDER_LENS_OUT";
|
|
1329
1642
|
function setupRerenderLens(options = {}, hooks = {}) {
|
|
1330
1643
|
ensureDevtoolsHook();
|