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