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