rerender-lens 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +118 -0
- package/README.md +192 -353
- package/dist/{budget-COBu7jBU.d.cts → budget-BnJlYwrV.d.ts} +20 -10
- package/dist/{budget-LkNjGtRc.d.ts → budget-CRswn-nC.d.cts} +20 -10
- package/dist/cli.cjs +248 -31
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +248 -31
- package/dist/cli.js.map +1 -1
- package/dist/{devtools-BHGUUo-p.d.ts → devtools-BYWat7nQ.d.ts} +17 -3
- package/dist/{devtools-DmhWiWcN.d.cts → devtools-DJi_6AfJ.d.cts} +17 -3
- package/dist/index.cjs +635 -109
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +131 -8
- package/dist/index.d.ts +131 -8
- package/dist/index.js +620 -110
- package/dist/index.js.map +1 -1
- package/dist/jest-setup.cjs +1673 -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 +1671 -0
- package/dist/jest-setup.js.map +1 -0
- package/dist/jest.cjs +1760 -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 +1751 -0
- package/dist/jest.js.map +1 -0
- package/dist/{notifiers-BjjGSHpp.d.ts → notifiers-DwKlvv4H.d.cts} +8 -4
- package/dist/{notifiers-BGQtWKfX.d.cts → notifiers-FNzKHs0a.d.ts} +8 -4
- package/dist/playwright.cjs +235 -30
- package/dist/playwright.cjs.map +1 -1
- package/dist/playwright.d.cts +8 -6
- package/dist/playwright.d.ts +8 -6
- package/dist/playwright.js +235 -31
- package/dist/playwright.js.map +1 -1
- package/dist/relay.cjs +10 -2
- package/dist/relay.cjs.map +1 -1
- package/dist/relay.js +10 -2
- package/dist/relay.js.map +1 -1
- package/dist/rerender-lens.iife.js +1100 -337
- package/dist/runner-report-B8Sc3FZO.d.cts +46 -0
- package/dist/runner-report-CW8X5RwK.d.ts +46 -0
- package/dist/setup.cjs +308 -95
- package/dist/setup.cjs.map +1 -1
- package/dist/setup.js +308 -95
- package/dist/setup.js.map +1 -1
- package/dist/{types-BzUEVkxJ.d.cts → types-DW5-N2XH.d.cts} +55 -2
- package/dist/{types-BzUEVkxJ.d.ts → types-DW5-N2XH.d.ts} +55 -2
- package/dist/vite.d.cts +1 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vitest-setup.cjs +452 -87
- 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 +452 -87
- package/dist/vitest-setup.js.map +1 -1
- package/dist/vitest.cjs +515 -123
- 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 +516 -124
- package/dist/vitest.js.map +1 -1
- package/package.json +41 -5
- package/panel/panel.css +1 -34
- package/panel/panel.js +865 -713
package/dist/vitest-setup.cjs
CHANGED
|
@@ -30,10 +30,49 @@ function remember(seen, a, b) {
|
|
|
30
30
|
set.add(b);
|
|
31
31
|
return false;
|
|
32
32
|
}
|
|
33
|
+
var scope = null;
|
|
34
|
+
var DEFAULT_DIFF_BUDGET = 5e4;
|
|
35
|
+
function beginDiffScope(budget = 2e5) {
|
|
36
|
+
const previous = scope;
|
|
37
|
+
scope = { cache: /* @__PURE__ */ new WeakMap(), remaining: budget, exhausted: false };
|
|
38
|
+
return () => {
|
|
39
|
+
scope = previous;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function diffBudgetExhausted() {
|
|
43
|
+
return scope?.exhausted === true;
|
|
44
|
+
}
|
|
33
45
|
function deepEqual(a, b, seen = /* @__PURE__ */ new Map()) {
|
|
34
46
|
if (Object.is(a, b)) return true;
|
|
35
47
|
if (typeof a !== typeof b) return false;
|
|
36
48
|
if (typeof a !== "object" || a === null || b === null) return false;
|
|
49
|
+
if (!scope) {
|
|
50
|
+
const end = beginDiffScope(DEFAULT_DIFF_BUDGET);
|
|
51
|
+
try {
|
|
52
|
+
return deepEqual(a, b, seen);
|
|
53
|
+
} finally {
|
|
54
|
+
end();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const sc = scope;
|
|
58
|
+
const objA = a;
|
|
59
|
+
const objB = b;
|
|
60
|
+
const hit = sc.cache.get(objA)?.get(objB);
|
|
61
|
+
if (hit !== void 0) return hit;
|
|
62
|
+
if (--sc.remaining < 0) {
|
|
63
|
+
sc.exhausted = true;
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const result = deepEqualObjects(objA, objB, seen);
|
|
67
|
+
let m = sc.cache.get(objA);
|
|
68
|
+
if (!m) {
|
|
69
|
+
m = /* @__PURE__ */ new WeakMap();
|
|
70
|
+
sc.cache.set(objA, m);
|
|
71
|
+
}
|
|
72
|
+
m.set(objB, result);
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
function deepEqualObjects(a, b, seen) {
|
|
37
76
|
const objA = a;
|
|
38
77
|
const objB = b;
|
|
39
78
|
if (remember(seen, objA, objB)) return true;
|
|
@@ -62,6 +101,10 @@ function deepEqual(a, b, seen = /* @__PURE__ */ new Map()) {
|
|
|
62
101
|
}
|
|
63
102
|
if (a instanceof Set) {
|
|
64
103
|
if (!(b instanceof Set) || a.size !== b.size) return false;
|
|
104
|
+
if (a.size > 50) {
|
|
105
|
+
for (const v of a) if (!b.has(v)) return false;
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
65
108
|
outer: for (const v of a) {
|
|
66
109
|
if (b.has(v)) continue;
|
|
67
110
|
for (const w of b) if (deepEqual(v, w, seen)) continue outer;
|
|
@@ -77,14 +120,16 @@ function deepEqual(a, b, seen = /* @__PURE__ */ new Map()) {
|
|
|
77
120
|
const ka = Object.keys(a);
|
|
78
121
|
const kb = Object.keys(b);
|
|
79
122
|
if (ka.length !== kb.length) return false;
|
|
123
|
+
const rb = b;
|
|
80
124
|
for (const k of ka) {
|
|
81
125
|
if (!Object.prototype.hasOwnProperty.call(b, k)) return false;
|
|
82
|
-
if (!deepEqual(a[k],
|
|
126
|
+
if (!deepEqual(a[k], rb[k], seen)) return false;
|
|
83
127
|
}
|
|
84
128
|
return true;
|
|
85
129
|
}
|
|
86
130
|
return false;
|
|
87
131
|
}
|
|
132
|
+
var AVOIDABLE_KINDS = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
|
|
88
133
|
function classify(prev, next) {
|
|
89
134
|
if (typeof prev === "function" && typeof next === "function") {
|
|
90
135
|
return prev.name === next.name && prev.toString() === next.toString() ? "function" : "different";
|
|
@@ -121,30 +166,30 @@ function diffRecords(prev, next, basePath = "") {
|
|
|
121
166
|
if (Object.is(a, b)) continue;
|
|
122
167
|
const kind = classify(a, b);
|
|
123
168
|
if (kind === "different") {
|
|
124
|
-
changes.push({ 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 });
|
|
125
170
|
} else {
|
|
126
171
|
changes.push({ path, kind, prev: a, next: b });
|
|
127
172
|
}
|
|
128
173
|
}
|
|
129
174
|
return changes;
|
|
130
175
|
}
|
|
131
|
-
function firstDifferentPath(a, b, path, depth = 0) {
|
|
132
|
-
if (depth > 8) return path;
|
|
176
|
+
function firstDifferentPath(a, b, path = "", depth = 0) {
|
|
177
|
+
if (depth > 8) return path || "(value)";
|
|
133
178
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
134
|
-
if (a.length !== b.length) return `${path}.length
|
|
179
|
+
if (a.length !== b.length) return path ? `${path}.length` : "length";
|
|
135
180
|
for (let i = 0; i < a.length; i++) {
|
|
136
181
|
if (!deepEqual(a[i], b[i])) return firstDifferentPath(a[i], b[i], joinPath(path, i), depth + 1);
|
|
137
182
|
}
|
|
138
|
-
return
|
|
183
|
+
return null;
|
|
139
184
|
}
|
|
140
185
|
if (isPlainObject(a) && isPlainObject(b)) {
|
|
141
186
|
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
142
187
|
for (const k of keys) {
|
|
143
188
|
if (!deepEqual(a[k], b[k])) return firstDifferentPath(a[k], b[k], joinPath(path, k), depth + 1);
|
|
144
189
|
}
|
|
145
|
-
return
|
|
190
|
+
return null;
|
|
146
191
|
}
|
|
147
|
-
return path;
|
|
192
|
+
return deepEqual(a, b) ? null : path || "(value)";
|
|
148
193
|
}
|
|
149
194
|
|
|
150
195
|
// src/report.ts
|
|
@@ -162,10 +207,12 @@ function storeAdvice(custom) {
|
|
|
162
207
|
}
|
|
163
208
|
var isGenuine = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
|
|
164
209
|
var isChildren = (change) => change.path === "children";
|
|
210
|
+
var isRefObject = (change) => change.path === "ref" && change.kind === "deep-equal";
|
|
165
211
|
function fixFor(change) {
|
|
166
212
|
if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
|
|
167
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`;
|
|
168
214
|
}
|
|
215
|
+
if (isRefObject(change)) return `create the ref once with useRef (or createRef outside the render) and pass the same object every time`;
|
|
169
216
|
switch (change.kind) {
|
|
170
217
|
case "function":
|
|
171
218
|
return `wrap it in useCallback (or hoist it out of the parent's render)`;
|
|
@@ -181,6 +228,7 @@ function describe(change) {
|
|
|
181
228
|
if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
|
|
182
229
|
return `children are new React elements with the same types and props on every render of the parent`;
|
|
183
230
|
}
|
|
231
|
+
if (isRefObject(change)) return `prop "ref" is a new ref object on every render (React re-attaches it each time)`;
|
|
184
232
|
switch (change.kind) {
|
|
185
233
|
case "deep-equal":
|
|
186
234
|
return `prop "${change.path}" is a new reference but deep-equal to the previous value`;
|
|
@@ -208,11 +256,21 @@ function buildReport(input) {
|
|
|
208
256
|
if (causes.length === 0) trigger = "parent";
|
|
209
257
|
else if (causes.length === 1) trigger = causes[0];
|
|
210
258
|
else trigger = "mixed";
|
|
211
|
-
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;
|
|
212
263
|
const reasons = [];
|
|
213
|
-
|
|
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) {
|
|
214
272
|
const who = input.parent ? `<${input.parent.name}> re-rendered (${describeTrigger(input.parent.trigger)})` : "its parent re-rendered";
|
|
215
|
-
reasons.push(`re-rendered with identical props because ${who}.
|
|
273
|
+
reasons.push(`re-rendered with identical props because ${who}. ${memoFix}.`);
|
|
216
274
|
} else if (avoidable && input.parent) {
|
|
217
275
|
reasons.push(`caused by <${input.parent.name}> re-rendering (${describeTrigger(input.parent.trigger)}).`);
|
|
218
276
|
}
|
|
@@ -221,9 +279,9 @@ function buildReport(input) {
|
|
|
221
279
|
reasons.push(fix ? `${describe(c)}: ${fix}.` : `${describe(c)}.`);
|
|
222
280
|
}
|
|
223
281
|
const memoized = input.memoized !== false;
|
|
224
|
-
if (avoidable && !memoized && input.propChanges.length > 0) {
|
|
282
|
+
if (avoidable && !memoized && !input.compiled && input.propChanges.length > 0) {
|
|
225
283
|
reasons.push(
|
|
226
|
-
`"${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)"}.`
|
|
227
285
|
);
|
|
228
286
|
}
|
|
229
287
|
for (const c of stateChanges) {
|
|
@@ -235,6 +293,8 @@ function buildReport(input) {
|
|
|
235
293
|
const where = c.provider && c.provider.component ? ` (provided by <${c.provider.component}>)` : "";
|
|
236
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` : "";
|
|
237
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.`);
|
|
238
298
|
} else if (isGenuine(c)) reasons.push(`${hookLabel(c)} changed.`);
|
|
239
299
|
else if (isStateHook(c))
|
|
240
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.`);
|
|
@@ -265,6 +325,7 @@ function buildReport(input) {
|
|
|
265
325
|
reasons,
|
|
266
326
|
time: now()
|
|
267
327
|
};
|
|
328
|
+
if (input.compiled) report.compiled = true;
|
|
268
329
|
if (input.selfDuration !== void 0) report.selfDuration = input.selfDuration;
|
|
269
330
|
if (input.treeDuration !== void 0) report.treeDuration = input.treeDuration;
|
|
270
331
|
if (input.commitPriority) report.commitPriority = input.commitPriority;
|
|
@@ -302,10 +363,10 @@ var KIND_LABEL = {
|
|
|
302
363
|
};
|
|
303
364
|
function summarize(report) {
|
|
304
365
|
const counts2 = /* @__PURE__ */ new Map();
|
|
305
|
-
for (const c of [...report.propChanges, ...report.stateChanges, ...report.hookChanges]) {
|
|
366
|
+
for (const c of [...report.propChanges || [], ...report.stateChanges || [], ...report.hookChanges || []]) {
|
|
306
367
|
counts2.set(c.kind, (counts2.get(c.kind) ?? 0) + 1);
|
|
307
368
|
}
|
|
308
|
-
const parts = [...counts2].map(([kind, n]) => `${n} ${KIND_LABEL[kind]}`);
|
|
369
|
+
const parts = [...counts2].map(([kind, n]) => `${n} ${KIND_LABEL[kind] || kind}`);
|
|
309
370
|
if (parts.length === 0) parts.push("no changes");
|
|
310
371
|
return parts.join(", ");
|
|
311
372
|
}
|
|
@@ -332,7 +393,7 @@ function getState() {
|
|
|
332
393
|
const g = globalThis;
|
|
333
394
|
let s = g[KEY];
|
|
334
395
|
if (!s) {
|
|
335
|
-
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 };
|
|
396
|
+
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 };
|
|
336
397
|
g[KEY] = s;
|
|
337
398
|
}
|
|
338
399
|
return s;
|
|
@@ -362,7 +423,7 @@ function dispatch(report, override) {
|
|
|
362
423
|
try {
|
|
363
424
|
options.notifier(report);
|
|
364
425
|
} catch (err) {
|
|
365
|
-
(
|
|
426
|
+
warnOnce("notifier", `notifier threw: ${String(err)}`);
|
|
366
427
|
}
|
|
367
428
|
}
|
|
368
429
|
}
|
|
@@ -429,6 +490,27 @@ function disable() {
|
|
|
429
490
|
}
|
|
430
491
|
|
|
431
492
|
// src/hookNames.ts
|
|
493
|
+
var NODE_COUNT = {
|
|
494
|
+
useState: 1,
|
|
495
|
+
useReducer: 1,
|
|
496
|
+
useRef: 1,
|
|
497
|
+
useMemo: 1,
|
|
498
|
+
useCallback: 1,
|
|
499
|
+
useEffect: 1,
|
|
500
|
+
useLayoutEffect: 1,
|
|
501
|
+
useInsertionEffect: 1,
|
|
502
|
+
useImperativeHandle: 1,
|
|
503
|
+
useSyncExternalStore: 2,
|
|
504
|
+
useTransition: 2,
|
|
505
|
+
useDeferredValue: 1,
|
|
506
|
+
useId: 1,
|
|
507
|
+
useOptimistic: 1,
|
|
508
|
+
useActionState: 3,
|
|
509
|
+
useFormState: 3,
|
|
510
|
+
useEffectEvent: 1,
|
|
511
|
+
useCacheRefresh: 1
|
|
512
|
+
};
|
|
513
|
+
var BUILTIN_HOOKS = /* @__PURE__ */ new Set([...Object.keys(NODE_COUNT), "useContext", "useDebugValue", "use"]);
|
|
432
514
|
var noop = () => {
|
|
433
515
|
};
|
|
434
516
|
function customHooksFromStack(stack, componentName, marker) {
|
|
@@ -443,7 +525,7 @@ function customHooksFromStack(stack, componentName, marker) {
|
|
|
443
525
|
continue;
|
|
444
526
|
}
|
|
445
527
|
if (name === componentName || name.endsWith("." + componentName)) break;
|
|
446
|
-
if (/^use[A-Z0-9_]/.test(name)) names.push(name);
|
|
528
|
+
if (/^use[A-Z0-9_]/.test(name) && !BUILTIN_HOOKS.has(name)) names.push(name);
|
|
447
529
|
if (names.length > 8) break;
|
|
448
530
|
}
|
|
449
531
|
return names;
|
|
@@ -727,6 +809,11 @@ function isMemoizedFiber(fiber) {
|
|
|
727
809
|
}
|
|
728
810
|
return false;
|
|
729
811
|
}
|
|
812
|
+
function isCompiledFiber(fiber) {
|
|
813
|
+
if (fiber.tag === ClassComponent) return false;
|
|
814
|
+
const cache2 = fiber.updateQueue?.memoCache;
|
|
815
|
+
return !!cache2 && Array.isArray(cache2.data);
|
|
816
|
+
}
|
|
730
817
|
function durationsOf(fiber) {
|
|
731
818
|
if (typeof fiber.actualDuration !== "number") return null;
|
|
732
819
|
const tree = fiber.actualDuration;
|
|
@@ -750,11 +837,14 @@ var counts = /* @__PURE__ */ new WeakMap();
|
|
|
750
837
|
var ids = /* @__PURE__ */ new WeakMap();
|
|
751
838
|
var fibersById = /* @__PURE__ */ new Map();
|
|
752
839
|
var hasWeakRef = typeof WeakRef === "function";
|
|
840
|
+
var pruneAt = 5e3;
|
|
753
841
|
function remember2(id, fiber) {
|
|
754
842
|
if (!hasWeakRef) return;
|
|
843
|
+
if (fibersById.get(id)?.deref() === fiber) return;
|
|
755
844
|
fibersById.set(id, new WeakRef(fiber));
|
|
756
|
-
if (fibersById.size >
|
|
845
|
+
if (fibersById.size > pruneAt) {
|
|
757
846
|
for (const [k, ref] of fibersById) if (!ref.deref()) fibersById.delete(k);
|
|
847
|
+
pruneAt = Math.max(5e3, fibersById.size * 2);
|
|
758
848
|
}
|
|
759
849
|
}
|
|
760
850
|
function instanceIdOf(fiber) {
|
|
@@ -783,7 +873,7 @@ function nearestComponent(fiber) {
|
|
|
783
873
|
}
|
|
784
874
|
var INTERNAL_FRAME = /react-dom|react_jsx|jsx-(dev-)?runtime|\/react\/|node_modules\/react|react-stack-bottom-frame|react_stack_bottom_frame|scheduler/;
|
|
785
875
|
function parseStackLocation(stack) {
|
|
786
|
-
for (const line of stack.split("\n")) {
|
|
876
|
+
for (const line of stack.split("\n", 24)) {
|
|
787
877
|
const m = /(?:at\s+(?:.*?\s+)?\(?|@)?((?:https?|file|webpack|vite|blob):[^\s()]+?):(\d+):(\d+)\)?\s*$/.exec(line.trim());
|
|
788
878
|
if (!m || !m[1] || INTERNAL_FRAME.test(m[1])) continue;
|
|
789
879
|
return { fileName: m[1], lineNumber: Number(m[2]), columnNumber: Number(m[3]) };
|
|
@@ -799,9 +889,16 @@ function sourceOf(fiber) {
|
|
|
799
889
|
return out;
|
|
800
890
|
}
|
|
801
891
|
const st = fiber._debugStack;
|
|
802
|
-
|
|
803
|
-
|
|
892
|
+
if (st && typeof st === "object") {
|
|
893
|
+
if (sourceCache.has(st)) return sourceCache.get(st);
|
|
894
|
+
const text = typeof st.stack === "string" ? st.stack : null;
|
|
895
|
+
const out = text ? parseStackLocation(text) : void 0;
|
|
896
|
+
sourceCache.set(st, out);
|
|
897
|
+
return out;
|
|
898
|
+
}
|
|
899
|
+
return typeof st === "string" ? parseStackLocation(st) : void 0;
|
|
804
900
|
}
|
|
901
|
+
var sourceCache = /* @__PURE__ */ new WeakMap();
|
|
805
902
|
function bumpCount(fiber) {
|
|
806
903
|
const prev = counts.get(fiber) ?? (fiber.alternate ? counts.get(fiber.alternate) : void 0) ?? 0;
|
|
807
904
|
const n = prev + 1;
|
|
@@ -869,7 +966,8 @@ function diffHooks(fiber, alt) {
|
|
|
869
966
|
const labels = hookLabelsFor(fiber, b);
|
|
870
967
|
let i = 0;
|
|
871
968
|
while (a && b) {
|
|
872
|
-
|
|
969
|
+
const deferred = labels?.[i] === "useDeferredValue";
|
|
970
|
+
if ((deferred || isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
|
|
873
971
|
const hook = labels?.[i] ?? hookLabel2(b);
|
|
874
972
|
out.push({
|
|
875
973
|
path: `${hook}#${i}`,
|
|
@@ -903,6 +1001,10 @@ function diffContexts(fiber, alt) {
|
|
|
903
1001
|
let a = alt.dependencies?.firstContext ?? null;
|
|
904
1002
|
let b = fiber.dependencies?.firstContext ?? null;
|
|
905
1003
|
let i = 0;
|
|
1004
|
+
if (b && !("memoizedValue" in b)) {
|
|
1005
|
+
warnOnce("contextValues", "this React does not record context values on the fiber (React < 18): context changes are reported as plain parent re-renders.");
|
|
1006
|
+
return out;
|
|
1007
|
+
}
|
|
906
1008
|
while (a && b) {
|
|
907
1009
|
if (a.context === b.context && !Object.is(a.memoizedValue, b.memoizedValue)) {
|
|
908
1010
|
const name = b.context.displayName ?? "Context";
|
|
@@ -935,10 +1037,26 @@ function diffContexts(fiber, alt) {
|
|
|
935
1037
|
return out;
|
|
936
1038
|
}
|
|
937
1039
|
var isGenuine2 = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
|
|
1040
|
+
var isRefObject2 = (v) => {
|
|
1041
|
+
if (typeof v !== "object" || v === null) return false;
|
|
1042
|
+
const keys = Object.keys(v);
|
|
1043
|
+
return keys.length === 1 && keys[0] === "current";
|
|
1044
|
+
};
|
|
1045
|
+
function diffProps(prev, next) {
|
|
1046
|
+
if (prev === next) return [];
|
|
1047
|
+
const pr = prev.ref;
|
|
1048
|
+
const nr = next.ref;
|
|
1049
|
+
if (pr !== nr && isRefObject2(pr) && isRefObject2(nr)) {
|
|
1050
|
+
const { ref: _p, ...restPrev } = prev;
|
|
1051
|
+
const { ref: _n, ...restNext } = next;
|
|
1052
|
+
return [{ path: "ref", kind: "deep-equal", prev: pr, next: nr }, ...diffRecords(restPrev, restNext)];
|
|
1053
|
+
}
|
|
1054
|
+
return diffRecords(prev, next);
|
|
1055
|
+
}
|
|
938
1056
|
function analyze(fiber, alt, trackHooks) {
|
|
939
1057
|
const prevProps = alt.memoizedProps ?? {};
|
|
940
1058
|
const nextProps = fiber.memoizedProps ?? {};
|
|
941
|
-
const propChanges =
|
|
1059
|
+
const propChanges = diffProps(prevProps, nextProps);
|
|
942
1060
|
let stateChanges = [];
|
|
943
1061
|
let hookChanges = [];
|
|
944
1062
|
if (fiber.tag === ClassComponent) {
|
|
@@ -951,7 +1069,7 @@ function analyze(fiber, alt, trackHooks) {
|
|
|
951
1069
|
} else if (trackHooks) {
|
|
952
1070
|
hookChanges = [...diffHooks(fiber, alt), ...diffContexts(fiber, alt)];
|
|
953
1071
|
}
|
|
954
|
-
const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore";
|
|
1072
|
+
const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore" && h.hook !== "useDeferredValue";
|
|
955
1073
|
const causes = [];
|
|
956
1074
|
if (propChanges.some(isGenuine2)) causes.push("props");
|
|
957
1075
|
if (stateChanges.some(isGenuine2) || hookChanges.some((h) => isStateHook(h) && isGenuine2(h))) causes.push("state");
|
|
@@ -989,6 +1107,26 @@ function updatersOf(root) {
|
|
|
989
1107
|
}
|
|
990
1108
|
var lastCommit = null;
|
|
991
1109
|
var EFFECT_LOOP_WINDOW_MS = 50;
|
|
1110
|
+
var MAX_REPORTS_PER_COMMIT = 200;
|
|
1111
|
+
var COMMIT_TIME_BUDGET_MS = 25;
|
|
1112
|
+
function isDeferredCatchUp(fiber) {
|
|
1113
|
+
const types = fiber._debugHookTypes;
|
|
1114
|
+
if (!types || !types.includes("useDeferredValue") || !fiber.alternate) return false;
|
|
1115
|
+
const first = fiber.memoizedState;
|
|
1116
|
+
if (!isHookList(first)) return false;
|
|
1117
|
+
const labels = hookLabelsFor(fiber, first);
|
|
1118
|
+
if (!labels) return false;
|
|
1119
|
+
let a = fiber.alternate.memoizedState;
|
|
1120
|
+
let b = first;
|
|
1121
|
+
for (let i = 0; a && b; a = a.next, b = b.next, i++) {
|
|
1122
|
+
if (labels[i] === "useDeferredValue" && !Object.is(a.memoizedState, b.memoizedState)) return true;
|
|
1123
|
+
}
|
|
1124
|
+
return false;
|
|
1125
|
+
}
|
|
1126
|
+
function isUnder(fiber, boundaries) {
|
|
1127
|
+
for (let f = fiber.return; f && f.tag !== HostRoot; f = f.return) if (f.tag === SuspenseComponent && boundaries.includes(f)) return true;
|
|
1128
|
+
return false;
|
|
1129
|
+
}
|
|
992
1130
|
function onCommit(root, commitPriority) {
|
|
993
1131
|
const s = getState();
|
|
994
1132
|
if (!s.enabled) return;
|
|
@@ -998,7 +1136,7 @@ function onCommit(root, commitPriority) {
|
|
|
998
1136
|
const rendered = [];
|
|
999
1137
|
const renderedNames = /* @__PURE__ */ new Set();
|
|
1000
1138
|
let hot = false;
|
|
1001
|
-
let
|
|
1139
|
+
let resolvedBoundaries = null;
|
|
1002
1140
|
const stack = [root.current];
|
|
1003
1141
|
while (stack.length) {
|
|
1004
1142
|
const fiber = stack.pop();
|
|
@@ -1008,7 +1146,7 @@ function onCommit(root, commitPriority) {
|
|
|
1008
1146
|
renderedNames.add(fiberName(fiber));
|
|
1009
1147
|
if (shouldTrack(fiberType(fiber), o)) rendered.push(fiber);
|
|
1010
1148
|
}
|
|
1011
|
-
if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null)
|
|
1149
|
+
if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) (resolvedBoundaries ?? (resolvedBoundaries = [])).push(fiber);
|
|
1012
1150
|
if (!alt || fiber.child !== alt.child) {
|
|
1013
1151
|
for (let c = fiber.child; c; c = c.sibling) stack.push(c);
|
|
1014
1152
|
}
|
|
@@ -1019,10 +1157,11 @@ function onCommit(root, commitPriority) {
|
|
|
1019
1157
|
const updaters = updatersOf(root);
|
|
1020
1158
|
let commitCause;
|
|
1021
1159
|
let afterCommit;
|
|
1022
|
-
|
|
1160
|
+
const inputDriven = commitPriority === "immediate" || commitPriority === "user-blocking";
|
|
1161
|
+
if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u)) && !rendered.some(isDeferredCatchUp)) {
|
|
1023
1162
|
commitCause = "effect-after-commit";
|
|
1024
1163
|
afterCommit = lastCommit.id;
|
|
1025
|
-
} else if (
|
|
1164
|
+
} else if (resolvedBoundaries) commitCause = "suspense-resolved";
|
|
1026
1165
|
if (rendered.length === 0) {
|
|
1027
1166
|
lastCommit = { id: s.nextCommitId, at, rendered: renderedNames };
|
|
1028
1167
|
return;
|
|
@@ -1031,82 +1170,262 @@ function onCommit(root, commitPriority) {
|
|
|
1031
1170
|
lastCommit = { id: commitId, at, rendered: renderedNames };
|
|
1032
1171
|
const dispatcherRef = o.resolveHookNames ? getDispatcherRef() : null;
|
|
1033
1172
|
const parentCache = /* @__PURE__ */ new Map();
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1173
|
+
const endDiffScope = beginDiffScope();
|
|
1174
|
+
const deadline = at + COMMIT_TIME_BUDGET_MS;
|
|
1175
|
+
try {
|
|
1176
|
+
for (let i = 0; i < rendered.length; i++) {
|
|
1177
|
+
if (i >= MAX_REPORTS_PER_COMMIT || (i & 15) === 15 && nowMs() > deadline) {
|
|
1178
|
+
s.truncated += rendered.length - i;
|
|
1179
|
+
warnOnce("truncated", `a commit rendered ${rendered.length} tracked components; only ${i} were reported (see info().truncated). Narrow \`include\` or turn off includeState.`);
|
|
1180
|
+
break;
|
|
1181
|
+
}
|
|
1182
|
+
const fiber = rendered[i];
|
|
1183
|
+
const alt = fiber.alternate;
|
|
1184
|
+
const a = analyze(fiber, alt, trackHooks);
|
|
1185
|
+
const durations = durationsOf(fiber);
|
|
1186
|
+
let hookState = o.includeState !== false && trackHooks ? snapshotHooks(fiber) : void 0;
|
|
1187
|
+
if (dispatcherRef && fiber.tag !== ClassComponent && (a.hookChanges.length || hookState && hookState.length)) {
|
|
1188
|
+
const names = resolveHookNames(fiber, dispatcherRef);
|
|
1189
|
+
if (names.size) {
|
|
1190
|
+
for (const c of a.hookChanges) if (c.hook !== "useContext" && names.has(c.index)) c.custom = names.get(c.index);
|
|
1191
|
+
if (hookState) hookState = hookState.map((h) => names.has(h.index) ? { ...h, custom: names.get(h.index) } : h);
|
|
1192
|
+
}
|
|
1044
1193
|
}
|
|
1194
|
+
const report = buildReport({
|
|
1195
|
+
component: fiberName(fiber),
|
|
1196
|
+
instanceId: instanceId(fiber),
|
|
1197
|
+
renderCount: bumpCount(fiber),
|
|
1198
|
+
prevProps: alt.memoizedProps ?? {},
|
|
1199
|
+
nextProps: fiber.memoizedProps ?? {},
|
|
1200
|
+
propChanges: a.propChanges,
|
|
1201
|
+
stateChanges: a.stateChanges,
|
|
1202
|
+
hookChanges: a.hookChanges,
|
|
1203
|
+
...o.includeState !== false ? {
|
|
1204
|
+
hookState,
|
|
1205
|
+
contexts: trackHooks ? snapshotContexts(fiber) : void 0,
|
|
1206
|
+
state: fiber.tag === ClassComponent && fiber.memoizedState && typeof fiber.memoizedState === "object" ? fiber.memoizedState : void 0
|
|
1207
|
+
} : {},
|
|
1208
|
+
updaters,
|
|
1209
|
+
commitCause,
|
|
1210
|
+
afterCommit,
|
|
1211
|
+
key: fiber.key === null || fiber.key === void 0 ? null : String(fiber.key),
|
|
1212
|
+
parent: a.trigger === "parent" ? nearestRenderedAncestor(fiber, parentCache, trackHooks) : null,
|
|
1213
|
+
owner: ownerName(fiber),
|
|
1214
|
+
path: componentPath(fiber),
|
|
1215
|
+
memoized: isMemoizedFiber(fiber),
|
|
1216
|
+
compiled: isCompiledFiber(fiber),
|
|
1217
|
+
classComponent: fiber.tag === ClassComponent,
|
|
1218
|
+
revealed: resolvedBoundaries !== null && a.trigger === "parent" && isUnder(fiber, resolvedBoundaries),
|
|
1219
|
+
selfDuration: durations ? durations.self : void 0,
|
|
1220
|
+
treeDuration: durations ? durations.tree : void 0,
|
|
1221
|
+
commitId,
|
|
1222
|
+
commitPriority,
|
|
1223
|
+
source: sourceOf(fiber)
|
|
1224
|
+
});
|
|
1225
|
+
dispatch(report);
|
|
1045
1226
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1227
|
+
} finally {
|
|
1228
|
+
endDiffScope();
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// src/causes.ts
|
|
1233
|
+
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);
|
|
1234
|
+
var bump = (m, key, n = 1) => void m.set(key, (m.get(key) || 0) + n);
|
|
1235
|
+
function indexByComponent(reports) {
|
|
1236
|
+
const index = /* @__PURE__ */ new Map();
|
|
1237
|
+
for (const r of reports) {
|
|
1238
|
+
const list = index.get(r.component);
|
|
1239
|
+
if (list) list.push(r);
|
|
1240
|
+
else index.set(r.component, [r]);
|
|
1241
|
+
}
|
|
1242
|
+
return index;
|
|
1243
|
+
}
|
|
1244
|
+
function rootCauseOf(r, commitReports, index = indexByComponent(commitReports)) {
|
|
1245
|
+
let cur = r;
|
|
1246
|
+
const seen = /* @__PURE__ */ new Set([r]);
|
|
1247
|
+
while (cur.trigger === "parent" && cur.parent) {
|
|
1248
|
+
const parent = cur.parent;
|
|
1249
|
+
const candidates = index.get(parent.name);
|
|
1250
|
+
const p = candidates && candidates.find((x) => isAncestorReport(x, cur));
|
|
1251
|
+
if (!p || seen.has(p)) return { name: parent.name, trigger: parent.trigger, report: null };
|
|
1252
|
+
seen.add(p);
|
|
1253
|
+
cur = p;
|
|
1254
|
+
}
|
|
1255
|
+
return cur === r ? null : { name: cur.component, trigger: cur.trigger, report: cur };
|
|
1256
|
+
}
|
|
1257
|
+
function rootCausesOf(reports) {
|
|
1258
|
+
const roots = /* @__PURE__ */ new Map();
|
|
1259
|
+
const rootByReport = /* @__PURE__ */ new Map();
|
|
1260
|
+
const index = indexByComponent(reports);
|
|
1261
|
+
let avoidable = 0;
|
|
1262
|
+
let wasted = 0;
|
|
1263
|
+
for (const r of reports) {
|
|
1264
|
+
if (!r.avoidable) continue;
|
|
1265
|
+
avoidable++;
|
|
1266
|
+
if (typeof r.selfDuration === "number") wasted += r.selfDuration;
|
|
1267
|
+
const root = rootCauseOf(r, reports, index);
|
|
1268
|
+
const name = root ? root.name : r.parent && r.parent.name || "(unknown)";
|
|
1269
|
+
const trigger = root ? root.trigger : r.parent && r.parent.trigger || "parent";
|
|
1270
|
+
rootByReport.set(r, name);
|
|
1271
|
+
let agg = roots.get(name);
|
|
1272
|
+
if (!agg) {
|
|
1273
|
+
agg = { name, trigger, count: 0, components: /* @__PURE__ */ new Map() };
|
|
1274
|
+
roots.set(name, agg);
|
|
1275
|
+
}
|
|
1276
|
+
agg.count++;
|
|
1277
|
+
bump(agg.components, r.component);
|
|
1075
1278
|
}
|
|
1279
|
+
return { roots: [...roots.values()].sort((a, b) => b.count - a.count), rootByReport, avoidable, wasted };
|
|
1280
|
+
}
|
|
1281
|
+
function groupByCommit(reports) {
|
|
1282
|
+
const out = /* @__PURE__ */ new Map();
|
|
1283
|
+
for (const r of reports) {
|
|
1284
|
+
if (typeof r.commitId !== "number" || r.commitId <= 0) continue;
|
|
1285
|
+
const list = out.get(r.commitId);
|
|
1286
|
+
if (list) list.push(r);
|
|
1287
|
+
else out.set(r.commitId, [r]);
|
|
1288
|
+
}
|
|
1289
|
+
return out;
|
|
1290
|
+
}
|
|
1291
|
+
function rankRootCauses(reports) {
|
|
1292
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1293
|
+
for (const commit of groupByCommit(reports).values()) {
|
|
1294
|
+
for (const root of rootCausesOf(commit).roots) {
|
|
1295
|
+
let agg = byName.get(root.name);
|
|
1296
|
+
if (!agg) {
|
|
1297
|
+
agg = { name: root.name, trigger: root.trigger, commits: 0, count: 0, components: /* @__PURE__ */ new Map() };
|
|
1298
|
+
byName.set(root.name, agg);
|
|
1299
|
+
}
|
|
1300
|
+
agg.commits++;
|
|
1301
|
+
agg.count += root.count;
|
|
1302
|
+
for (const [c, n] of root.components) bump(agg.components, c, n);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
return [...byName.values()].sort((a, b) => b.count - a.count || b.commits - a.commits || a.name.localeCompare(b.name));
|
|
1306
|
+
}
|
|
1307
|
+
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
1308
|
+
function formatRootCauses(reports, limit = 5) {
|
|
1309
|
+
const ranked = rankRootCauses(reports);
|
|
1310
|
+
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(", ")})`);
|
|
1311
|
+
if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
|
|
1312
|
+
return lines.join("\n");
|
|
1076
1313
|
}
|
|
1077
1314
|
|
|
1078
1315
|
// src/fixes.ts
|
|
1079
|
-
var
|
|
1316
|
+
var MAX_FIX_REPORTS = 50;
|
|
1080
1317
|
var rootOf = (path) => path.split(/[.[]/)[0] || path;
|
|
1318
|
+
var identifier = (name) => /^[A-Za-z_$][\w$]*$/.test(name) ? name : "value";
|
|
1319
|
+
function shortValue(v, max = 60) {
|
|
1320
|
+
let s;
|
|
1321
|
+
try {
|
|
1322
|
+
s = JSON.stringify(v);
|
|
1323
|
+
} catch {
|
|
1324
|
+
s = String(v);
|
|
1325
|
+
}
|
|
1326
|
+
if (s === void 0) s = String(v);
|
|
1327
|
+
return s.length > max ? s.slice(0, max - 3) + "..." : s;
|
|
1328
|
+
}
|
|
1081
1329
|
function fixesFor(r) {
|
|
1082
1330
|
const out = [];
|
|
1083
|
-
const
|
|
1084
|
-
const
|
|
1331
|
+
const ownerName2 = r.owner || r.parent && r.parent.name || null;
|
|
1332
|
+
const propChanges = r.propChanges || [];
|
|
1333
|
+
const changes = [...propChanges, ...r.stateChanges || [], ...r.hookChanges || []];
|
|
1085
1334
|
if (r.avoidable && (changes.length === 0 || r.memoized === false)) {
|
|
1335
|
+
const identical = changes.length === 0;
|
|
1086
1336
|
out.push({
|
|
1087
1337
|
kind: "memo",
|
|
1088
1338
|
owner: r.component,
|
|
1089
1339
|
target: r.component,
|
|
1090
1340
|
prop: null,
|
|
1091
1341
|
label: `Wrap <${r.component}> in React.memo`,
|
|
1092
|
-
detail:
|
|
1342
|
+
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.`,
|
|
1343
|
+
snippet: `// ${r.component}
|
|
1344
|
+
import { memo } from 'react';
|
|
1345
|
+
|
|
1346
|
+
export const ${r.component} = memo(function ${r.component}(props) {
|
|
1347
|
+
// ...
|
|
1348
|
+
});
|
|
1349
|
+
// class components: extend PureComponent instead`
|
|
1093
1350
|
});
|
|
1094
1351
|
}
|
|
1095
|
-
for (const c of
|
|
1096
|
-
if (!
|
|
1352
|
+
for (const c of propChanges) {
|
|
1353
|
+
if (!AVOIDABLE_KINDS.has(c.kind)) continue;
|
|
1354
|
+
const owner = ownerName2 || "?";
|
|
1097
1355
|
const root = rootOf(c.path);
|
|
1356
|
+
const id = identifier(root);
|
|
1098
1357
|
if (root === "children" && (c.kind === "element" || c.kind === "deep-equal")) {
|
|
1099
|
-
out.push({
|
|
1358
|
+
out.push({
|
|
1359
|
+
kind: "children",
|
|
1360
|
+
owner,
|
|
1361
|
+
target: r.component,
|
|
1362
|
+
prop: "children",
|
|
1363
|
+
label: `memoize children of <${r.component}> in <${owner}>`,
|
|
1364
|
+
detail: `<${owner}> re-creates the children of <${r.component}> on every render; they have the same types and props each time.`,
|
|
1365
|
+
snippet: `// ${owner}
|
|
1366
|
+
import { useMemo } from 'react';
|
|
1367
|
+
|
|
1368
|
+
const children = useMemo(() => (
|
|
1369
|
+
<>{/* the same elements */}</>
|
|
1370
|
+
), [/* deps */]);
|
|
1371
|
+
|
|
1372
|
+
<${r.component}>{children}</${r.component}>
|
|
1373
|
+
|
|
1374
|
+
// static children: hoist them to module scope
|
|
1375
|
+
const STATIC = <em>hi</em>;`
|
|
1376
|
+
});
|
|
1100
1377
|
} else if (c.kind === "function") {
|
|
1101
|
-
out.push({
|
|
1378
|
+
out.push({
|
|
1379
|
+
kind: "useCallback",
|
|
1380
|
+
owner,
|
|
1381
|
+
target: r.component,
|
|
1382
|
+
prop: root,
|
|
1383
|
+
label: `useCallback(${root}) in <${owner}>`,
|
|
1384
|
+
detail: `prop "${c.path}" of <${r.component}> is a new function on every render of <${owner}>.`,
|
|
1385
|
+
snippet: `// ${owner}
|
|
1386
|
+
import { useCallback } from 'react';
|
|
1387
|
+
|
|
1388
|
+
const ${id} = useCallback((/* args */) => {
|
|
1389
|
+
// ...
|
|
1390
|
+
}, [/* deps */]);
|
|
1391
|
+
|
|
1392
|
+
<${r.component} ${root}={${id}} />`
|
|
1393
|
+
});
|
|
1102
1394
|
} else if (c.kind === "element") {
|
|
1103
|
-
out.push({
|
|
1395
|
+
out.push({
|
|
1396
|
+
kind: "useMemoElement",
|
|
1397
|
+
owner,
|
|
1398
|
+
target: r.component,
|
|
1399
|
+
prop: root,
|
|
1400
|
+
label: `memoize element prop ${root} in <${owner}>`,
|
|
1401
|
+
detail: `prop "${c.path}" of <${r.component}> is a new element with the same type and props on every render of <${owner}>.`,
|
|
1402
|
+
snippet: `// ${owner}
|
|
1403
|
+
import { useMemo } from 'react';
|
|
1404
|
+
|
|
1405
|
+
const ${id} = useMemo(() => ${shortValue(c.next, 40)}, [/* deps */]);
|
|
1406
|
+
// or pass it as children from a component that does not re-render`
|
|
1407
|
+
});
|
|
1104
1408
|
} else {
|
|
1105
|
-
|
|
1409
|
+
const isArray = Array.isArray(c.next);
|
|
1410
|
+
out.push({
|
|
1411
|
+
kind: "useMemo",
|
|
1412
|
+
owner,
|
|
1413
|
+
target: r.component,
|
|
1414
|
+
prop: root,
|
|
1415
|
+
label: `useMemo(${root}) in <${owner}>`,
|
|
1416
|
+
detail: `prop "${c.path}" of <${r.component}> is a new ${isArray ? "array" : "object"} with the same contents on every render of <${owner}>.`,
|
|
1417
|
+
snippet: `// ${owner}
|
|
1418
|
+
import { useMemo } from 'react';
|
|
1419
|
+
|
|
1420
|
+
const ${id} = useMemo(() => (${shortValue(c.next, 80)}), [/* deps */]);
|
|
1421
|
+
|
|
1422
|
+
// or, when it never changes, hoist it to module scope:
|
|
1423
|
+
const ${id.toUpperCase()} = ${shortValue(c.next, 80)};`
|
|
1424
|
+
});
|
|
1106
1425
|
}
|
|
1107
1426
|
}
|
|
1108
|
-
for (const c of [...r.stateChanges, ...r.hookChanges]) {
|
|
1109
|
-
const isContext = c.hook === "useContext";
|
|
1427
|
+
for (const c of [...r.stateChanges || [], ...r.hookChanges || []]) {
|
|
1428
|
+
const isContext = c.hook === "useContext" || /^useContext/.test(c.path);
|
|
1110
1429
|
const ctxName = isContext ? (/useContext\((.*)\)/.exec(c.path) || [])[1] || "Context" : "";
|
|
1111
1430
|
const providerOwner = isContext && c.provider && c.provider.component ? c.provider.component : null;
|
|
1112
1431
|
if (isContext && c.kind === "different" && c.changedKeys && typeof c.totalKeys === "number" && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys) {
|
|
@@ -1116,20 +1435,63 @@ function fixesFor(r) {
|
|
|
1116
1435
|
target: r.component,
|
|
1117
1436
|
prop: ctxName,
|
|
1118
1437
|
label: `split ${ctxName}${providerOwner ? ` in <${providerOwner}>` : ""}: only ${c.changedKeys.join(", ")} changed`,
|
|
1119
|
-
detail: `${c.changedKeys.
|
|
1438
|
+
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.`,
|
|
1439
|
+
snippet: `// ${providerOwner || "Provider"}
|
|
1440
|
+
// one context per independently-changing slice
|
|
1441
|
+
const ${identifier(ctxName)}Static = createContext(...);
|
|
1442
|
+
const ${identifier(ctxName)}${c.changedKeys.map((k) => k[0].toUpperCase() + k.slice(1)).join("")} = createContext(...);
|
|
1443
|
+
|
|
1444
|
+
// or keep one context and let consumers select a slice:
|
|
1445
|
+
const ${c.changedKeys[0]} = useContextSelector(${ctxName}, (v) => v.${c.changedKeys[0]});`
|
|
1120
1446
|
});
|
|
1121
1447
|
continue;
|
|
1122
1448
|
}
|
|
1123
|
-
if (!
|
|
1449
|
+
if (!AVOIDABLE_KINDS.has(c.kind)) continue;
|
|
1124
1450
|
if (isContext) {
|
|
1125
|
-
out.push({
|
|
1451
|
+
out.push({
|
|
1452
|
+
kind: "contextValue",
|
|
1453
|
+
owner: providerOwner || `${ctxName}.Provider`,
|
|
1454
|
+
target: r.component,
|
|
1455
|
+
prop: ctxName,
|
|
1456
|
+
label: `memoize the ${ctxName} provider value${providerOwner ? ` in <${providerOwner}>` : ""}`,
|
|
1457
|
+
detail: `<${r.component}> re-rendered because ${ctxName} produced a new value that is deep-equal to the previous one${providerOwner ? ` (Provider rendered by <${providerOwner}>)` : ""}.`,
|
|
1458
|
+
snippet: `// ${providerOwner || `where <${ctxName}.Provider> is rendered`}
|
|
1459
|
+
const value = useMemo(() => ({ /* ... */ }), [/* deps */]);
|
|
1460
|
+
<${ctxName}.Provider value={value}>`
|
|
1461
|
+
});
|
|
1126
1462
|
} else if (c.hook === "useSyncExternalStore") {
|
|
1127
1463
|
const chain = c.custom || [];
|
|
1128
1464
|
const redux = chain.some((n) => /^use(App)?Selector$/.test(n));
|
|
1129
1465
|
const zustand = !redux && chain.some((n) => /^use[A-Z]\w*Store$/.test(n) || n === "useStore" || n === "useBoundStore");
|
|
1130
|
-
|
|
1466
|
+
const via = chain.length ? ` via ${chain.join(" \u203A ")}` : "";
|
|
1467
|
+
out.push({
|
|
1468
|
+
kind: "storeSnapshot",
|
|
1469
|
+
owner: r.component,
|
|
1470
|
+
target: r.component,
|
|
1471
|
+
prop: c.path,
|
|
1472
|
+
label: redux ? `memoize the selector in <${r.component}>` : zustand ? `useShallow in <${r.component}>` : `stable getSnapshot in <${r.component}>`,
|
|
1473
|
+
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.`,
|
|
1474
|
+
snippet: redux ? `// ${r.component}
|
|
1475
|
+
import { shallowEqual } from 'react-redux';
|
|
1476
|
+
const slice = useSelector(selectSlice, shallowEqual);
|
|
1477
|
+
// or memoize: const selectSlice = createSelector([selectA, selectB], (a, b) => ({ a, b }));` : zustand ? `// ${r.component}
|
|
1478
|
+
import { useShallow } from 'zustand/react/shallow';
|
|
1479
|
+
const { a, b } = useStore(useShallow((s) => ({ a: s.a, b: s.b })));
|
|
1480
|
+
// or select a primitive: const a = useStore((s) => s.a);` : `// ${r.component}
|
|
1481
|
+
// getSnapshot must return the same reference while the data is unchanged
|
|
1482
|
+
const snapshot = useSyncExternalStore(subscribe, store.getSnapshot /* cached */);`
|
|
1483
|
+
});
|
|
1131
1484
|
} else {
|
|
1132
|
-
out.push({
|
|
1485
|
+
out.push({
|
|
1486
|
+
kind: "bailout",
|
|
1487
|
+
owner: r.component,
|
|
1488
|
+
target: r.component,
|
|
1489
|
+
prop: c.path,
|
|
1490
|
+
label: `bail out before setting ${c.path} in <${r.component}>`,
|
|
1491
|
+
detail: `${c.path} was set to a value deep-equal to the current one (new reference).`,
|
|
1492
|
+
snippet: `// ${r.component}
|
|
1493
|
+
setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
1494
|
+
});
|
|
1133
1495
|
}
|
|
1134
1496
|
}
|
|
1135
1497
|
return out;
|
|
@@ -1143,11 +1505,12 @@ function rankFixes(reports) {
|
|
|
1143
1505
|
const key = fixKey(f);
|
|
1144
1506
|
let agg = byKey.get(key);
|
|
1145
1507
|
if (!agg) {
|
|
1146
|
-
agg = { ...f, key, count: 0, components: {} };
|
|
1508
|
+
agg = { ...f, key, count: 0, components: {}, reports: [] };
|
|
1147
1509
|
byKey.set(key, agg);
|
|
1148
1510
|
}
|
|
1149
1511
|
agg.count++;
|
|
1150
1512
|
agg.components[r.component] = (agg.components[r.component] || 0) + 1;
|
|
1513
|
+
if (agg.reports.length < MAX_FIX_REPORTS) agg.reports.push(r);
|
|
1151
1514
|
}
|
|
1152
1515
|
}
|
|
1153
1516
|
return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
|
|
@@ -1157,6 +1520,8 @@ function formatFixes(reports, limit = 10) {
|
|
|
1157
1520
|
if (!ranked.length) return "No avoidable re-renders.";
|
|
1158
1521
|
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(", ")})`);
|
|
1159
1522
|
if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
|
|
1523
|
+
const roots = formatRootCauses(reports, 5);
|
|
1524
|
+
if (roots) lines.push("", "Root causes:", roots);
|
|
1160
1525
|
return lines.join("\n");
|
|
1161
1526
|
}
|
|
1162
1527
|
|
|
@@ -1255,7 +1620,7 @@ ${formatFixes(reports)}`);
|
|
|
1255
1620
|
};
|
|
1256
1621
|
}
|
|
1257
1622
|
|
|
1258
|
-
// src/
|
|
1623
|
+
// src/runner-report.ts
|
|
1259
1624
|
var OUT_ENV = "RERENDER_LENS_OUT";
|
|
1260
1625
|
function setupRerenderLens(options = {}, hooks = {}) {
|
|
1261
1626
|
ensureDevtoolsHook();
|