rerender-lens 0.4.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 +78 -0
- package/README.md +192 -362
- 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-CZebzpF6.d.ts → devtools-BYWat7nQ.d.ts} +7 -1
- package/dist/{devtools-BkCct3cJ.d.cts → devtools-DJi_6AfJ.d.cts} +7 -1
- package/dist/index.cjs +439 -41
- 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 +424 -42
- 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 +230 -28
- package/dist/playwright.cjs.map +1 -1
- package/dist/playwright.d.cts +4 -4
- package/dist/playwright.d.ts +4 -4
- package/dist/playwright.js +230 -28
- package/dist/playwright.js.map +1 -1
- package/dist/relay.cjs +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 +356 -54
- package/dist/runner-report-B8Sc3FZO.d.cts +46 -0
- package/dist/runner-report-CW8X5RwK.d.ts +46 -0
- package/dist/setup.cjs +112 -27
- package/dist/setup.cjs.map +1 -1
- package/dist/setup.js +112 -27
- 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 +337 -41
- 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 +337 -41
- package/dist/vitest-setup.js.map +1 -1
- package/dist/vitest.cjs +400 -77
- 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 +401 -78
- package/dist/vitest.js.map +1 -1
- package/package.json +40 -4
- package/panel/panel.css +1 -0
- package/panel/panel.js +499 -309
package/dist/index.cjs
CHANGED
|
@@ -127,6 +127,7 @@ function deepEqualObjects(a, b, seen) {
|
|
|
127
127
|
}
|
|
128
128
|
return false;
|
|
129
129
|
}
|
|
130
|
+
var AVOIDABLE_KINDS = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
|
|
130
131
|
function classify(prev, next) {
|
|
131
132
|
if (typeof prev === "function" && typeof next === "function") {
|
|
132
133
|
return prev.name === next.name && prev.toString() === next.toString() ? "function" : "different";
|
|
@@ -163,30 +164,47 @@ function diffRecords(prev, next, basePath = "") {
|
|
|
163
164
|
if (Object.is(a, b)) continue;
|
|
164
165
|
const kind = classify(a, b);
|
|
165
166
|
if (kind === "different") {
|
|
166
|
-
changes.push({ path: diffBudgetExhausted() ? path : firstDifferentPath(a, b, path), kind, prev: a, next: b });
|
|
167
|
+
changes.push({ path: diffBudgetExhausted() ? path : firstDifferentPath(a, b, path) ?? path, kind, prev: a, next: b });
|
|
167
168
|
} else {
|
|
168
169
|
changes.push({ path, kind, prev: a, next: b });
|
|
169
170
|
}
|
|
170
171
|
}
|
|
171
172
|
return changes;
|
|
172
173
|
}
|
|
173
|
-
function firstDifferentPath(a, b, path, depth = 0) {
|
|
174
|
-
if (depth > 8) return path;
|
|
174
|
+
function firstDifferentPath(a, b, path = "", depth = 0) {
|
|
175
|
+
if (depth > 8) return path || "(value)";
|
|
175
176
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
176
|
-
if (a.length !== b.length) return `${path}.length
|
|
177
|
+
if (a.length !== b.length) return path ? `${path}.length` : "length";
|
|
177
178
|
for (let i = 0; i < a.length; i++) {
|
|
178
179
|
if (!deepEqual(a[i], b[i])) return firstDifferentPath(a[i], b[i], joinPath(path, i), depth + 1);
|
|
179
180
|
}
|
|
180
|
-
return
|
|
181
|
+
return null;
|
|
181
182
|
}
|
|
182
183
|
if (isPlainObject(a) && isPlainObject(b)) {
|
|
183
184
|
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
184
185
|
for (const k of keys) {
|
|
185
186
|
if (!deepEqual(a[k], b[k])) return firstDifferentPath(a[k], b[k], joinPath(path, k), depth + 1);
|
|
186
187
|
}
|
|
187
|
-
return
|
|
188
|
+
return null;
|
|
188
189
|
}
|
|
189
|
-
return path;
|
|
190
|
+
return deepEqual(a, b) ? null : path || "(value)";
|
|
191
|
+
}
|
|
192
|
+
function diffLeaves(a, b, limit = 20, path = "", out = []) {
|
|
193
|
+
if (out.length >= limit || Object.is(a, b)) return out;
|
|
194
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
195
|
+
const n = Math.max(a.length, b.length);
|
|
196
|
+
for (let i = 0; i < n && out.length < limit; i++) diffLeaves(a[i], b[i], limit, joinPath(path, i), out);
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
200
|
+
for (const k of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) {
|
|
201
|
+
if (out.length >= limit) break;
|
|
202
|
+
diffLeaves(a[k], b[k], limit, joinPath(path, k), out);
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
if (!deepEqual(a, b)) out.push({ path: path || "(value)", prev: a, next: b });
|
|
207
|
+
return out;
|
|
190
208
|
}
|
|
191
209
|
|
|
192
210
|
// src/report.ts
|
|
@@ -204,10 +222,12 @@ function storeAdvice(custom) {
|
|
|
204
222
|
}
|
|
205
223
|
var isGenuine = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
|
|
206
224
|
var isChildren = (change) => change.path === "children";
|
|
225
|
+
var isRefObject = (change) => change.path === "ref" && change.kind === "deep-equal";
|
|
207
226
|
function fixFor(change) {
|
|
208
227
|
if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
|
|
209
228
|
return `lift the children out of the parent's render: memoize them with useMemo, hoist static elements to module scope, or render them from a component that does not re-render`;
|
|
210
229
|
}
|
|
230
|
+
if (isRefObject(change)) return `create the ref once with useRef (or createRef outside the render) and pass the same object every time`;
|
|
211
231
|
switch (change.kind) {
|
|
212
232
|
case "function":
|
|
213
233
|
return `wrap it in useCallback (or hoist it out of the parent's render)`;
|
|
@@ -223,6 +243,7 @@ function describe(change) {
|
|
|
223
243
|
if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
|
|
224
244
|
return `children are new React elements with the same types and props on every render of the parent`;
|
|
225
245
|
}
|
|
246
|
+
if (isRefObject(change)) return `prop "ref" is a new ref object on every render (React re-attaches it each time)`;
|
|
226
247
|
switch (change.kind) {
|
|
227
248
|
case "deep-equal":
|
|
228
249
|
return `prop "${change.path}" is a new reference but deep-equal to the previous value`;
|
|
@@ -250,11 +271,21 @@ function buildReport(input) {
|
|
|
250
271
|
if (causes.length === 0) trigger = "parent";
|
|
251
272
|
else if (causes.length === 1) trigger = causes[0];
|
|
252
273
|
else trigger = "mixed";
|
|
253
|
-
const
|
|
274
|
+
const nothingChanged = input.propChanges.length === 0 && stateChanges.length === 0 && hookChanges.length === 0;
|
|
275
|
+
const cacheHit = input.compiled === true && trigger === "parent" && input.propChanges.length === 0;
|
|
276
|
+
const revealed = input.revealed === true && trigger === "parent";
|
|
277
|
+
const avoidable = trigger === "parent" && !cacheHit && !revealed;
|
|
254
278
|
const reasons = [];
|
|
255
|
-
|
|
279
|
+
const memoFix = input.classComponent ? `Make "${input.component}" extend PureComponent (or implement shouldComponentUpdate)` : `Wrap "${input.component}" in React.memo (or extend PureComponent)`;
|
|
280
|
+
if (cacheHit) {
|
|
281
|
+
reasons.push(`compiled by React Compiler: its output is memoized and the inputs did not change, so this render is cheap; nothing to fix.`);
|
|
282
|
+
} else if (revealed) {
|
|
283
|
+
reasons.push(
|
|
284
|
+
`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.`
|
|
285
|
+
);
|
|
286
|
+
} else if (avoidable && nothingChanged) {
|
|
256
287
|
const who = input.parent ? `<${input.parent.name}> re-rendered (${describeTrigger(input.parent.trigger)})` : "its parent re-rendered";
|
|
257
|
-
reasons.push(`re-rendered with identical props because ${who}.
|
|
288
|
+
reasons.push(`re-rendered with identical props because ${who}. ${memoFix}.`);
|
|
258
289
|
} else if (avoidable && input.parent) {
|
|
259
290
|
reasons.push(`caused by <${input.parent.name}> re-rendering (${describeTrigger(input.parent.trigger)}).`);
|
|
260
291
|
}
|
|
@@ -263,9 +294,9 @@ function buildReport(input) {
|
|
|
263
294
|
reasons.push(fix ? `${describe(c)}: ${fix}.` : `${describe(c)}.`);
|
|
264
295
|
}
|
|
265
296
|
const memoized = input.memoized !== false;
|
|
266
|
-
if (avoidable && !memoized && input.propChanges.length > 0) {
|
|
297
|
+
if (avoidable && !memoized && !input.compiled && input.propChanges.length > 0) {
|
|
267
298
|
reasons.push(
|
|
268
|
-
`"${input.component}" is not memoized, so fixing the props alone will not stop this re-render: also wrap it in React.memo (or extend PureComponent).`
|
|
299
|
+
`"${input.component}" is not memoized, so fixing the props alone will not stop this re-render: also ${input.classComponent ? "extend PureComponent (or implement shouldComponentUpdate)" : "wrap it in React.memo (or extend PureComponent)"}.`
|
|
269
300
|
);
|
|
270
301
|
}
|
|
271
302
|
for (const c of stateChanges) {
|
|
@@ -277,6 +308,8 @@ function buildReport(input) {
|
|
|
277
308
|
const where = c.provider && c.provider.component ? ` (provided by <${c.provider.component}>)` : "";
|
|
278
309
|
const keys = c.changedKeys && typeof c.totalKeys === "number" && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys ? `: only ${c.changedKeys.map((k) => `"${k}"`).join(", ")} of ${c.totalKeys} keys changed, yet every consumer re-renders. Split the context or memoize the slices consumers read` : "";
|
|
279
310
|
reasons.push(`${c.path} changed${where}${keys}.`);
|
|
311
|
+
} else if (c.hook === "useDeferredValue" && isGenuine(c)) {
|
|
312
|
+
reasons.push(`${hookLabel(c)} caught up with its latest value: this is the deferred second render React schedules after the urgent one; nothing to fix.`);
|
|
280
313
|
} else if (isGenuine(c)) reasons.push(`${hookLabel(c)} changed.`);
|
|
281
314
|
else if (isStateHook(c))
|
|
282
315
|
reasons.push(`${hookLabel(c)} was set to a value deep-equal to the current one (new reference, same contents): reuse the existing object or bail out before calling the setter.`);
|
|
@@ -307,6 +340,7 @@ function buildReport(input) {
|
|
|
307
340
|
reasons,
|
|
308
341
|
time: now()
|
|
309
342
|
};
|
|
343
|
+
if (input.compiled) report.compiled = true;
|
|
310
344
|
if (input.selfDuration !== void 0) report.selfDuration = input.selfDuration;
|
|
311
345
|
if (input.treeDuration !== void 0) report.treeDuration = input.treeDuration;
|
|
312
346
|
if (input.commitPriority) report.commitPriority = input.commitPriority;
|
|
@@ -344,10 +378,10 @@ var KIND_LABEL = {
|
|
|
344
378
|
};
|
|
345
379
|
function summarize(report) {
|
|
346
380
|
const counts2 = /* @__PURE__ */ new Map();
|
|
347
|
-
for (const c of [...report.propChanges, ...report.stateChanges, ...report.hookChanges]) {
|
|
381
|
+
for (const c of [...report.propChanges || [], ...report.stateChanges || [], ...report.hookChanges || []]) {
|
|
348
382
|
counts2.set(c.kind, (counts2.get(c.kind) ?? 0) + 1);
|
|
349
383
|
}
|
|
350
|
-
const parts = [...counts2].map(([kind, n]) => `${n} ${KIND_LABEL[kind]}`);
|
|
384
|
+
const parts = [...counts2].map(([kind, n]) => `${n} ${KIND_LABEL[kind] || kind}`);
|
|
351
385
|
if (parts.length === 0) parts.push("no changes");
|
|
352
386
|
return parts.join(", ");
|
|
353
387
|
}
|
|
@@ -410,6 +444,27 @@ function dispatch(report, override) {
|
|
|
410
444
|
}
|
|
411
445
|
|
|
412
446
|
// src/hookNames.ts
|
|
447
|
+
var NODE_COUNT = {
|
|
448
|
+
useState: 1,
|
|
449
|
+
useReducer: 1,
|
|
450
|
+
useRef: 1,
|
|
451
|
+
useMemo: 1,
|
|
452
|
+
useCallback: 1,
|
|
453
|
+
useEffect: 1,
|
|
454
|
+
useLayoutEffect: 1,
|
|
455
|
+
useInsertionEffect: 1,
|
|
456
|
+
useImperativeHandle: 1,
|
|
457
|
+
useSyncExternalStore: 2,
|
|
458
|
+
useTransition: 2,
|
|
459
|
+
useDeferredValue: 1,
|
|
460
|
+
useId: 1,
|
|
461
|
+
useOptimistic: 1,
|
|
462
|
+
useActionState: 3,
|
|
463
|
+
useFormState: 3,
|
|
464
|
+
useEffectEvent: 1,
|
|
465
|
+
useCacheRefresh: 1
|
|
466
|
+
};
|
|
467
|
+
var BUILTIN_HOOKS = /* @__PURE__ */ new Set([...Object.keys(NODE_COUNT), "useContext", "useDebugValue", "use"]);
|
|
413
468
|
var noop = () => {
|
|
414
469
|
};
|
|
415
470
|
function customHooksFromStack(stack, componentName, marker) {
|
|
@@ -424,7 +479,7 @@ function customHooksFromStack(stack, componentName, marker) {
|
|
|
424
479
|
continue;
|
|
425
480
|
}
|
|
426
481
|
if (name === componentName || name.endsWith("." + componentName)) break;
|
|
427
|
-
if (/^use[A-Z0-9_]/.test(name)) names.push(name);
|
|
482
|
+
if (/^use[A-Z0-9_]/.test(name) && !BUILTIN_HOOKS.has(name)) names.push(name);
|
|
428
483
|
if (names.length > 8) break;
|
|
429
484
|
}
|
|
430
485
|
return names;
|
|
@@ -724,6 +779,11 @@ function isMemoizedFiber(fiber) {
|
|
|
724
779
|
}
|
|
725
780
|
return false;
|
|
726
781
|
}
|
|
782
|
+
function isCompiledFiber(fiber) {
|
|
783
|
+
if (fiber.tag === ClassComponent) return false;
|
|
784
|
+
const cache2 = fiber.updateQueue?.memoCache;
|
|
785
|
+
return !!cache2 && Array.isArray(cache2.data);
|
|
786
|
+
}
|
|
727
787
|
function durationsOf(fiber) {
|
|
728
788
|
if (typeof fiber.actualDuration !== "number") return null;
|
|
729
789
|
const tree = fiber.actualDuration;
|
|
@@ -902,7 +962,8 @@ function diffHooks(fiber, alt) {
|
|
|
902
962
|
const labels = hookLabelsFor(fiber, b);
|
|
903
963
|
let i = 0;
|
|
904
964
|
while (a && b) {
|
|
905
|
-
|
|
965
|
+
const deferred = labels?.[i] === "useDeferredValue";
|
|
966
|
+
if ((deferred || isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
|
|
906
967
|
const hook = labels?.[i] ?? hookLabel2(b);
|
|
907
968
|
out.push({
|
|
908
969
|
path: `${hook}#${i}`,
|
|
@@ -936,6 +997,10 @@ function diffContexts(fiber, alt) {
|
|
|
936
997
|
let a = alt.dependencies?.firstContext ?? null;
|
|
937
998
|
let b = fiber.dependencies?.firstContext ?? null;
|
|
938
999
|
let i = 0;
|
|
1000
|
+
if (b && !("memoizedValue" in b)) {
|
|
1001
|
+
warnOnce("contextValues", "this React does not record context values on the fiber (React < 18): context changes are reported as plain parent re-renders.");
|
|
1002
|
+
return out;
|
|
1003
|
+
}
|
|
939
1004
|
while (a && b) {
|
|
940
1005
|
if (a.context === b.context && !Object.is(a.memoizedValue, b.memoizedValue)) {
|
|
941
1006
|
const name = b.context.displayName ?? "Context";
|
|
@@ -968,10 +1033,26 @@ function diffContexts(fiber, alt) {
|
|
|
968
1033
|
return out;
|
|
969
1034
|
}
|
|
970
1035
|
var isGenuine2 = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
|
|
1036
|
+
var isRefObject2 = (v) => {
|
|
1037
|
+
if (typeof v !== "object" || v === null) return false;
|
|
1038
|
+
const keys = Object.keys(v);
|
|
1039
|
+
return keys.length === 1 && keys[0] === "current";
|
|
1040
|
+
};
|
|
1041
|
+
function diffProps(prev, next) {
|
|
1042
|
+
if (prev === next) return [];
|
|
1043
|
+
const pr = prev.ref;
|
|
1044
|
+
const nr = next.ref;
|
|
1045
|
+
if (pr !== nr && isRefObject2(pr) && isRefObject2(nr)) {
|
|
1046
|
+
const { ref: _p, ...restPrev } = prev;
|
|
1047
|
+
const { ref: _n, ...restNext } = next;
|
|
1048
|
+
return [{ path: "ref", kind: "deep-equal", prev: pr, next: nr }, ...diffRecords(restPrev, restNext)];
|
|
1049
|
+
}
|
|
1050
|
+
return diffRecords(prev, next);
|
|
1051
|
+
}
|
|
971
1052
|
function analyze(fiber, alt, trackHooks) {
|
|
972
1053
|
const prevProps = alt.memoizedProps ?? {};
|
|
973
1054
|
const nextProps = fiber.memoizedProps ?? {};
|
|
974
|
-
const propChanges =
|
|
1055
|
+
const propChanges = diffProps(prevProps, nextProps);
|
|
975
1056
|
let stateChanges = [];
|
|
976
1057
|
let hookChanges = [];
|
|
977
1058
|
if (fiber.tag === ClassComponent) {
|
|
@@ -984,7 +1065,7 @@ function analyze(fiber, alt, trackHooks) {
|
|
|
984
1065
|
} else if (trackHooks) {
|
|
985
1066
|
hookChanges = [...diffHooks(fiber, alt), ...diffContexts(fiber, alt)];
|
|
986
1067
|
}
|
|
987
|
-
const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore";
|
|
1068
|
+
const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore" && h.hook !== "useDeferredValue";
|
|
988
1069
|
const causes = [];
|
|
989
1070
|
if (propChanges.some(isGenuine2)) causes.push("props");
|
|
990
1071
|
if (stateChanges.some(isGenuine2) || hookChanges.some((h) => isStateHook(h) && isGenuine2(h))) causes.push("state");
|
|
@@ -1024,6 +1105,24 @@ var lastCommit = null;
|
|
|
1024
1105
|
var EFFECT_LOOP_WINDOW_MS = 50;
|
|
1025
1106
|
var MAX_REPORTS_PER_COMMIT = 200;
|
|
1026
1107
|
var COMMIT_TIME_BUDGET_MS = 25;
|
|
1108
|
+
function isDeferredCatchUp(fiber) {
|
|
1109
|
+
const types = fiber._debugHookTypes;
|
|
1110
|
+
if (!types || !types.includes("useDeferredValue") || !fiber.alternate) return false;
|
|
1111
|
+
const first = fiber.memoizedState;
|
|
1112
|
+
if (!isHookList(first)) return false;
|
|
1113
|
+
const labels = hookLabelsFor(fiber, first);
|
|
1114
|
+
if (!labels) return false;
|
|
1115
|
+
let a = fiber.alternate.memoizedState;
|
|
1116
|
+
let b = first;
|
|
1117
|
+
for (let i = 0; a && b; a = a.next, b = b.next, i++) {
|
|
1118
|
+
if (labels[i] === "useDeferredValue" && !Object.is(a.memoizedState, b.memoizedState)) return true;
|
|
1119
|
+
}
|
|
1120
|
+
return false;
|
|
1121
|
+
}
|
|
1122
|
+
function isUnder(fiber, boundaries) {
|
|
1123
|
+
for (let f = fiber.return; f && f.tag !== HostRoot; f = f.return) if (f.tag === SuspenseComponent && boundaries.includes(f)) return true;
|
|
1124
|
+
return false;
|
|
1125
|
+
}
|
|
1027
1126
|
function onCommit(root, commitPriority) {
|
|
1028
1127
|
const s = getState();
|
|
1029
1128
|
if (!s.enabled) return;
|
|
@@ -1033,7 +1132,7 @@ function onCommit(root, commitPriority) {
|
|
|
1033
1132
|
const rendered = [];
|
|
1034
1133
|
const renderedNames = /* @__PURE__ */ new Set();
|
|
1035
1134
|
let hot = false;
|
|
1036
|
-
let
|
|
1135
|
+
let resolvedBoundaries = null;
|
|
1037
1136
|
const stack = [root.current];
|
|
1038
1137
|
while (stack.length) {
|
|
1039
1138
|
const fiber = stack.pop();
|
|
@@ -1043,7 +1142,7 @@ function onCommit(root, commitPriority) {
|
|
|
1043
1142
|
renderedNames.add(fiberName(fiber));
|
|
1044
1143
|
if (shouldTrack(fiberType(fiber), o)) rendered.push(fiber);
|
|
1045
1144
|
}
|
|
1046
|
-
if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null)
|
|
1145
|
+
if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) (resolvedBoundaries ?? (resolvedBoundaries = [])).push(fiber);
|
|
1047
1146
|
if (!alt || fiber.child !== alt.child) {
|
|
1048
1147
|
for (let c = fiber.child; c; c = c.sibling) stack.push(c);
|
|
1049
1148
|
}
|
|
@@ -1055,10 +1154,10 @@ function onCommit(root, commitPriority) {
|
|
|
1055
1154
|
let commitCause;
|
|
1056
1155
|
let afterCommit;
|
|
1057
1156
|
const inputDriven = commitPriority === "immediate" || commitPriority === "user-blocking";
|
|
1058
|
-
if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u))) {
|
|
1157
|
+
if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u)) && !rendered.some(isDeferredCatchUp)) {
|
|
1059
1158
|
commitCause = "effect-after-commit";
|
|
1060
1159
|
afterCommit = lastCommit.id;
|
|
1061
|
-
} else if (
|
|
1160
|
+
} else if (resolvedBoundaries) commitCause = "suspense-resolved";
|
|
1062
1161
|
if (rendered.length === 0) {
|
|
1063
1162
|
lastCommit = { id: s.nextCommitId, at, rendered: renderedNames };
|
|
1064
1163
|
return;
|
|
@@ -1110,6 +1209,9 @@ function onCommit(root, commitPriority) {
|
|
|
1110
1209
|
owner: ownerName(fiber),
|
|
1111
1210
|
path: componentPath(fiber),
|
|
1112
1211
|
memoized: isMemoizedFiber(fiber),
|
|
1212
|
+
compiled: isCompiledFiber(fiber),
|
|
1213
|
+
classComponent: fiber.tag === ClassComponent,
|
|
1214
|
+
revealed: resolvedBoundaries !== null && a.trigger === "parent" && isUnder(fiber, resolvedBoundaries),
|
|
1113
1215
|
selfDuration: durations ? durations.self : void 0,
|
|
1114
1216
|
treeDuration: durations ? durations.tree : void 0,
|
|
1115
1217
|
commitId,
|
|
@@ -1221,38 +1323,270 @@ function useWhyRerender(name, values, options) {
|
|
|
1221
1323
|
});
|
|
1222
1324
|
}
|
|
1223
1325
|
|
|
1326
|
+
// src/causes.ts
|
|
1327
|
+
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);
|
|
1328
|
+
var bump = (m, key, n = 1) => void m.set(key, (m.get(key) || 0) + n);
|
|
1329
|
+
function indexByComponent(reports) {
|
|
1330
|
+
const index = /* @__PURE__ */ new Map();
|
|
1331
|
+
for (const r of reports) {
|
|
1332
|
+
const list = index.get(r.component);
|
|
1333
|
+
if (list) list.push(r);
|
|
1334
|
+
else index.set(r.component, [r]);
|
|
1335
|
+
}
|
|
1336
|
+
return index;
|
|
1337
|
+
}
|
|
1338
|
+
function rootCauseOf(r, commitReports, index = indexByComponent(commitReports)) {
|
|
1339
|
+
let cur = r;
|
|
1340
|
+
const seen = /* @__PURE__ */ new Set([r]);
|
|
1341
|
+
while (cur.trigger === "parent" && cur.parent) {
|
|
1342
|
+
const parent = cur.parent;
|
|
1343
|
+
const candidates = index.get(parent.name);
|
|
1344
|
+
const p = candidates && candidates.find((x) => isAncestorReport(x, cur));
|
|
1345
|
+
if (!p || seen.has(p)) return { name: parent.name, trigger: parent.trigger, report: null };
|
|
1346
|
+
seen.add(p);
|
|
1347
|
+
cur = p;
|
|
1348
|
+
}
|
|
1349
|
+
return cur === r ? null : { name: cur.component, trigger: cur.trigger, report: cur };
|
|
1350
|
+
}
|
|
1351
|
+
function rootCausesOf(reports) {
|
|
1352
|
+
const roots = /* @__PURE__ */ new Map();
|
|
1353
|
+
const rootByReport = /* @__PURE__ */ new Map();
|
|
1354
|
+
const index = indexByComponent(reports);
|
|
1355
|
+
let avoidable = 0;
|
|
1356
|
+
let wasted = 0;
|
|
1357
|
+
for (const r of reports) {
|
|
1358
|
+
if (!r.avoidable) continue;
|
|
1359
|
+
avoidable++;
|
|
1360
|
+
if (typeof r.selfDuration === "number") wasted += r.selfDuration;
|
|
1361
|
+
const root = rootCauseOf(r, reports, index);
|
|
1362
|
+
const name = root ? root.name : r.parent && r.parent.name || "(unknown)";
|
|
1363
|
+
const trigger = root ? root.trigger : r.parent && r.parent.trigger || "parent";
|
|
1364
|
+
rootByReport.set(r, name);
|
|
1365
|
+
let agg = roots.get(name);
|
|
1366
|
+
if (!agg) {
|
|
1367
|
+
agg = { name, trigger, count: 0, components: /* @__PURE__ */ new Map() };
|
|
1368
|
+
roots.set(name, agg);
|
|
1369
|
+
}
|
|
1370
|
+
agg.count++;
|
|
1371
|
+
bump(agg.components, r.component);
|
|
1372
|
+
}
|
|
1373
|
+
return { roots: [...roots.values()].sort((a, b) => b.count - a.count), rootByReport, avoidable, wasted };
|
|
1374
|
+
}
|
|
1375
|
+
function analyzeCommit(reports) {
|
|
1376
|
+
const first = reports[0];
|
|
1377
|
+
return {
|
|
1378
|
+
id: first && typeof first.commitId === "number" ? first.commitId : 0,
|
|
1379
|
+
total: reports.length,
|
|
1380
|
+
...rootCausesOf(reports),
|
|
1381
|
+
contexts: contextAttribution(reports),
|
|
1382
|
+
reports
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
function contextAttribution(reports) {
|
|
1386
|
+
const byCtx = /* @__PURE__ */ new Map();
|
|
1387
|
+
for (const r of reports) {
|
|
1388
|
+
for (const c of r.hookChanges || []) {
|
|
1389
|
+
if (c.hook !== "useContext" && !/^useContext/.test(c.path)) continue;
|
|
1390
|
+
const m = /useContext\((.*)\)/.exec(c.path);
|
|
1391
|
+
const name = m && m[1] ? m[1] : c.path;
|
|
1392
|
+
let agg = byCtx.get(name);
|
|
1393
|
+
if (!agg) {
|
|
1394
|
+
agg = { name, consumers: 0, avoidable: 0, components: /* @__PURE__ */ new Map(), commits: /* @__PURE__ */ new Set(), providers: /* @__PURE__ */ new Map(), changedKeys: /* @__PURE__ */ new Set(), totalKeys: 0 };
|
|
1395
|
+
byCtx.set(name, agg);
|
|
1396
|
+
}
|
|
1397
|
+
agg.consumers++;
|
|
1398
|
+
if (AVOIDABLE_KINDS.has(c.kind)) agg.avoidable++;
|
|
1399
|
+
bump(agg.components, r.component);
|
|
1400
|
+
agg.commits.add(typeof r.commitId === "number" ? r.commitId : 0);
|
|
1401
|
+
if (c.provider && c.provider.component) bump(agg.providers, c.provider.component);
|
|
1402
|
+
if (c.changedKeys) for (const k of c.changedKeys) agg.changedKeys.add(k);
|
|
1403
|
+
if (typeof c.totalKeys === "number") agg.totalKeys = Math.max(agg.totalKeys, c.totalKeys);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
return [...byCtx.values()].sort((a, b) => b.consumers - a.consumers);
|
|
1407
|
+
}
|
|
1408
|
+
function cascadeTree(reports) {
|
|
1409
|
+
const root = { name: "", children: /* @__PURE__ */ new Map(), report: null };
|
|
1410
|
+
for (const r of reports) {
|
|
1411
|
+
let node = root;
|
|
1412
|
+
for (const seg of r.path.concat([r.component])) {
|
|
1413
|
+
let next = node.children.get(seg);
|
|
1414
|
+
if (!next) {
|
|
1415
|
+
next = { name: seg, children: /* @__PURE__ */ new Map(), report: null };
|
|
1416
|
+
node.children.set(seg, next);
|
|
1417
|
+
}
|
|
1418
|
+
node = next;
|
|
1419
|
+
}
|
|
1420
|
+
if (!node.report || r.avoidable) node.report = r;
|
|
1421
|
+
node.count = (node.count || 0) + 1;
|
|
1422
|
+
if (r.avoidable) node.avoidable = (node.avoidable || 0) + 1;
|
|
1423
|
+
}
|
|
1424
|
+
return root;
|
|
1425
|
+
}
|
|
1426
|
+
function rootCauseSummary(name, commits, analyze2) {
|
|
1427
|
+
const analyzeFn = analyze2 || ((_, reports) => analyzeCommit(reports));
|
|
1428
|
+
const out = { name, trigger: "parent", commits: [], total: 0, components: /* @__PURE__ */ new Map(), affected: [] };
|
|
1429
|
+
for (const [key, reports] of commits) {
|
|
1430
|
+
const analysis = analyzeFn(key, reports);
|
|
1431
|
+
const root = analysis.roots.find((x) => x.name === name);
|
|
1432
|
+
if (!root) continue;
|
|
1433
|
+
out.trigger = root.trigger;
|
|
1434
|
+
out.commits.push({ key, analysis, count: root.count, components: root.components });
|
|
1435
|
+
out.total += root.count;
|
|
1436
|
+
for (const [c, n] of root.components) bump(out.components, c, n);
|
|
1437
|
+
for (const r of reports) if (r.avoidable && analysis.rootByReport.get(r) === name) out.affected.push(r);
|
|
1438
|
+
}
|
|
1439
|
+
out.commits.reverse();
|
|
1440
|
+
return out;
|
|
1441
|
+
}
|
|
1442
|
+
function groupByCommit(reports) {
|
|
1443
|
+
const out = /* @__PURE__ */ new Map();
|
|
1444
|
+
for (const r of reports) {
|
|
1445
|
+
if (typeof r.commitId !== "number" || r.commitId <= 0) continue;
|
|
1446
|
+
const list = out.get(r.commitId);
|
|
1447
|
+
if (list) list.push(r);
|
|
1448
|
+
else out.set(r.commitId, [r]);
|
|
1449
|
+
}
|
|
1450
|
+
return out;
|
|
1451
|
+
}
|
|
1452
|
+
function rankRootCauses(reports) {
|
|
1453
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1454
|
+
for (const commit of groupByCommit(reports).values()) {
|
|
1455
|
+
for (const root of rootCausesOf(commit).roots) {
|
|
1456
|
+
let agg = byName.get(root.name);
|
|
1457
|
+
if (!agg) {
|
|
1458
|
+
agg = { name: root.name, trigger: root.trigger, commits: 0, count: 0, components: /* @__PURE__ */ new Map() };
|
|
1459
|
+
byName.set(root.name, agg);
|
|
1460
|
+
}
|
|
1461
|
+
agg.commits++;
|
|
1462
|
+
agg.count += root.count;
|
|
1463
|
+
for (const [c, n] of root.components) bump(agg.components, c, n);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
return [...byName.values()].sort((a, b) => b.count - a.count || b.commits - a.commits || a.name.localeCompare(b.name));
|
|
1467
|
+
}
|
|
1468
|
+
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
1469
|
+
function formatRootCauses(reports, limit = 5) {
|
|
1470
|
+
const ranked = rankRootCauses(reports);
|
|
1471
|
+
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(", ")})`);
|
|
1472
|
+
if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
|
|
1473
|
+
return lines.join("\n");
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1224
1476
|
// src/fixes.ts
|
|
1225
|
-
var
|
|
1477
|
+
var MAX_FIX_REPORTS = 50;
|
|
1226
1478
|
var rootOf = (path) => path.split(/[.[]/)[0] || path;
|
|
1479
|
+
var identifier = (name) => /^[A-Za-z_$][\w$]*$/.test(name) ? name : "value";
|
|
1480
|
+
function shortValue(v, max = 60) {
|
|
1481
|
+
let s;
|
|
1482
|
+
try {
|
|
1483
|
+
s = JSON.stringify(v);
|
|
1484
|
+
} catch {
|
|
1485
|
+
s = String(v);
|
|
1486
|
+
}
|
|
1487
|
+
if (s === void 0) s = String(v);
|
|
1488
|
+
return s.length > max ? s.slice(0, max - 3) + "..." : s;
|
|
1489
|
+
}
|
|
1227
1490
|
function fixesFor(r) {
|
|
1228
1491
|
const out = [];
|
|
1229
|
-
const
|
|
1230
|
-
const
|
|
1492
|
+
const ownerName2 = r.owner || r.parent && r.parent.name || null;
|
|
1493
|
+
const propChanges = r.propChanges || [];
|
|
1494
|
+
const changes = [...propChanges, ...r.stateChanges || [], ...r.hookChanges || []];
|
|
1231
1495
|
if (r.avoidable && (changes.length === 0 || r.memoized === false)) {
|
|
1496
|
+
const identical = changes.length === 0;
|
|
1232
1497
|
out.push({
|
|
1233
1498
|
kind: "memo",
|
|
1234
1499
|
owner: r.component,
|
|
1235
1500
|
target: r.component,
|
|
1236
1501
|
prop: null,
|
|
1237
1502
|
label: `Wrap <${r.component}> in React.memo`,
|
|
1238
|
-
detail:
|
|
1503
|
+
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.`,
|
|
1504
|
+
snippet: `// ${r.component}
|
|
1505
|
+
import { memo } from 'react';
|
|
1506
|
+
|
|
1507
|
+
export const ${r.component} = memo(function ${r.component}(props) {
|
|
1508
|
+
// ...
|
|
1509
|
+
});
|
|
1510
|
+
// class components: extend PureComponent instead`
|
|
1239
1511
|
});
|
|
1240
1512
|
}
|
|
1241
|
-
for (const c of
|
|
1242
|
-
if (!
|
|
1513
|
+
for (const c of propChanges) {
|
|
1514
|
+
if (!AVOIDABLE_KINDS.has(c.kind)) continue;
|
|
1515
|
+
const owner = ownerName2 || "?";
|
|
1243
1516
|
const root = rootOf(c.path);
|
|
1517
|
+
const id = identifier(root);
|
|
1244
1518
|
if (root === "children" && (c.kind === "element" || c.kind === "deep-equal")) {
|
|
1245
|
-
out.push({
|
|
1519
|
+
out.push({
|
|
1520
|
+
kind: "children",
|
|
1521
|
+
owner,
|
|
1522
|
+
target: r.component,
|
|
1523
|
+
prop: "children",
|
|
1524
|
+
label: `memoize children of <${r.component}> in <${owner}>`,
|
|
1525
|
+
detail: `<${owner}> re-creates the children of <${r.component}> on every render; they have the same types and props each time.`,
|
|
1526
|
+
snippet: `// ${owner}
|
|
1527
|
+
import { useMemo } from 'react';
|
|
1528
|
+
|
|
1529
|
+
const children = useMemo(() => (
|
|
1530
|
+
<>{/* the same elements */}</>
|
|
1531
|
+
), [/* deps */]);
|
|
1532
|
+
|
|
1533
|
+
<${r.component}>{children}</${r.component}>
|
|
1534
|
+
|
|
1535
|
+
// static children: hoist them to module scope
|
|
1536
|
+
const STATIC = <em>hi</em>;`
|
|
1537
|
+
});
|
|
1246
1538
|
} else if (c.kind === "function") {
|
|
1247
|
-
out.push({
|
|
1539
|
+
out.push({
|
|
1540
|
+
kind: "useCallback",
|
|
1541
|
+
owner,
|
|
1542
|
+
target: r.component,
|
|
1543
|
+
prop: root,
|
|
1544
|
+
label: `useCallback(${root}) in <${owner}>`,
|
|
1545
|
+
detail: `prop "${c.path}" of <${r.component}> is a new function on every render of <${owner}>.`,
|
|
1546
|
+
snippet: `// ${owner}
|
|
1547
|
+
import { useCallback } from 'react';
|
|
1548
|
+
|
|
1549
|
+
const ${id} = useCallback((/* args */) => {
|
|
1550
|
+
// ...
|
|
1551
|
+
}, [/* deps */]);
|
|
1552
|
+
|
|
1553
|
+
<${r.component} ${root}={${id}} />`
|
|
1554
|
+
});
|
|
1248
1555
|
} else if (c.kind === "element") {
|
|
1249
|
-
out.push({
|
|
1556
|
+
out.push({
|
|
1557
|
+
kind: "useMemoElement",
|
|
1558
|
+
owner,
|
|
1559
|
+
target: r.component,
|
|
1560
|
+
prop: root,
|
|
1561
|
+
label: `memoize element prop ${root} in <${owner}>`,
|
|
1562
|
+
detail: `prop "${c.path}" of <${r.component}> is a new element with the same type and props on every render of <${owner}>.`,
|
|
1563
|
+
snippet: `// ${owner}
|
|
1564
|
+
import { useMemo } from 'react';
|
|
1565
|
+
|
|
1566
|
+
const ${id} = useMemo(() => ${shortValue(c.next, 40)}, [/* deps */]);
|
|
1567
|
+
// or pass it as children from a component that does not re-render`
|
|
1568
|
+
});
|
|
1250
1569
|
} else {
|
|
1251
|
-
|
|
1570
|
+
const isArray = Array.isArray(c.next);
|
|
1571
|
+
out.push({
|
|
1572
|
+
kind: "useMemo",
|
|
1573
|
+
owner,
|
|
1574
|
+
target: r.component,
|
|
1575
|
+
prop: root,
|
|
1576
|
+
label: `useMemo(${root}) in <${owner}>`,
|
|
1577
|
+
detail: `prop "${c.path}" of <${r.component}> is a new ${isArray ? "array" : "object"} with the same contents on every render of <${owner}>.`,
|
|
1578
|
+
snippet: `// ${owner}
|
|
1579
|
+
import { useMemo } from 'react';
|
|
1580
|
+
|
|
1581
|
+
const ${id} = useMemo(() => (${shortValue(c.next, 80)}), [/* deps */]);
|
|
1582
|
+
|
|
1583
|
+
// or, when it never changes, hoist it to module scope:
|
|
1584
|
+
const ${id.toUpperCase()} = ${shortValue(c.next, 80)};`
|
|
1585
|
+
});
|
|
1252
1586
|
}
|
|
1253
1587
|
}
|
|
1254
|
-
for (const c of [...r.stateChanges, ...r.hookChanges]) {
|
|
1255
|
-
const isContext = c.hook === "useContext";
|
|
1588
|
+
for (const c of [...r.stateChanges || [], ...r.hookChanges || []]) {
|
|
1589
|
+
const isContext = c.hook === "useContext" || /^useContext/.test(c.path);
|
|
1256
1590
|
const ctxName = isContext ? (/useContext\((.*)\)/.exec(c.path) || [])[1] || "Context" : "";
|
|
1257
1591
|
const providerOwner = isContext && c.provider && c.provider.component ? c.provider.component : null;
|
|
1258
1592
|
if (isContext && c.kind === "different" && c.changedKeys && typeof c.totalKeys === "number" && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys) {
|
|
@@ -1262,20 +1596,63 @@ function fixesFor(r) {
|
|
|
1262
1596
|
target: r.component,
|
|
1263
1597
|
prop: ctxName,
|
|
1264
1598
|
label: `split ${ctxName}${providerOwner ? ` in <${providerOwner}>` : ""}: only ${c.changedKeys.join(", ")} changed`,
|
|
1265
|
-
detail: `${c.changedKeys.
|
|
1599
|
+
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.`,
|
|
1600
|
+
snippet: `// ${providerOwner || "Provider"}
|
|
1601
|
+
// one context per independently-changing slice
|
|
1602
|
+
const ${identifier(ctxName)}Static = createContext(...);
|
|
1603
|
+
const ${identifier(ctxName)}${c.changedKeys.map((k) => k[0].toUpperCase() + k.slice(1)).join("")} = createContext(...);
|
|
1604
|
+
|
|
1605
|
+
// or keep one context and let consumers select a slice:
|
|
1606
|
+
const ${c.changedKeys[0]} = useContextSelector(${ctxName}, (v) => v.${c.changedKeys[0]});`
|
|
1266
1607
|
});
|
|
1267
1608
|
continue;
|
|
1268
1609
|
}
|
|
1269
|
-
if (!
|
|
1610
|
+
if (!AVOIDABLE_KINDS.has(c.kind)) continue;
|
|
1270
1611
|
if (isContext) {
|
|
1271
|
-
out.push({
|
|
1612
|
+
out.push({
|
|
1613
|
+
kind: "contextValue",
|
|
1614
|
+
owner: providerOwner || `${ctxName}.Provider`,
|
|
1615
|
+
target: r.component,
|
|
1616
|
+
prop: ctxName,
|
|
1617
|
+
label: `memoize the ${ctxName} provider value${providerOwner ? ` in <${providerOwner}>` : ""}`,
|
|
1618
|
+
detail: `<${r.component}> re-rendered because ${ctxName} produced a new value that is deep-equal to the previous one${providerOwner ? ` (Provider rendered by <${providerOwner}>)` : ""}.`,
|
|
1619
|
+
snippet: `// ${providerOwner || `where <${ctxName}.Provider> is rendered`}
|
|
1620
|
+
const value = useMemo(() => ({ /* ... */ }), [/* deps */]);
|
|
1621
|
+
<${ctxName}.Provider value={value}>`
|
|
1622
|
+
});
|
|
1272
1623
|
} else if (c.hook === "useSyncExternalStore") {
|
|
1273
1624
|
const chain = c.custom || [];
|
|
1274
1625
|
const redux = chain.some((n) => /^use(App)?Selector$/.test(n));
|
|
1275
1626
|
const zustand = !redux && chain.some((n) => /^use[A-Z]\w*Store$/.test(n) || n === "useStore" || n === "useBoundStore");
|
|
1276
|
-
|
|
1627
|
+
const via = chain.length ? ` via ${chain.join(" \u203A ")}` : "";
|
|
1628
|
+
out.push({
|
|
1629
|
+
kind: "storeSnapshot",
|
|
1630
|
+
owner: r.component,
|
|
1631
|
+
target: r.component,
|
|
1632
|
+
prop: c.path,
|
|
1633
|
+
label: redux ? `memoize the selector in <${r.component}>` : zustand ? `useShallow in <${r.component}>` : `stable getSnapshot in <${r.component}>`,
|
|
1634
|
+
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.`,
|
|
1635
|
+
snippet: redux ? `// ${r.component}
|
|
1636
|
+
import { shallowEqual } from 'react-redux';
|
|
1637
|
+
const slice = useSelector(selectSlice, shallowEqual);
|
|
1638
|
+
// or memoize: const selectSlice = createSelector([selectA, selectB], (a, b) => ({ a, b }));` : zustand ? `// ${r.component}
|
|
1639
|
+
import { useShallow } from 'zustand/react/shallow';
|
|
1640
|
+
const { a, b } = useStore(useShallow((s) => ({ a: s.a, b: s.b })));
|
|
1641
|
+
// or select a primitive: const a = useStore((s) => s.a);` : `// ${r.component}
|
|
1642
|
+
// getSnapshot must return the same reference while the data is unchanged
|
|
1643
|
+
const snapshot = useSyncExternalStore(subscribe, store.getSnapshot /* cached */);`
|
|
1644
|
+
});
|
|
1277
1645
|
} else {
|
|
1278
|
-
out.push({
|
|
1646
|
+
out.push({
|
|
1647
|
+
kind: "bailout",
|
|
1648
|
+
owner: r.component,
|
|
1649
|
+
target: r.component,
|
|
1650
|
+
prop: c.path,
|
|
1651
|
+
label: `bail out before setting ${c.path} in <${r.component}>`,
|
|
1652
|
+
detail: `${c.path} was set to a value deep-equal to the current one (new reference).`,
|
|
1653
|
+
snippet: `// ${r.component}
|
|
1654
|
+
setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
1655
|
+
});
|
|
1279
1656
|
}
|
|
1280
1657
|
}
|
|
1281
1658
|
return out;
|
|
@@ -1289,11 +1666,12 @@ function rankFixes(reports) {
|
|
|
1289
1666
|
const key = fixKey(f);
|
|
1290
1667
|
let agg = byKey.get(key);
|
|
1291
1668
|
if (!agg) {
|
|
1292
|
-
agg = { ...f, key, count: 0, components: {} };
|
|
1669
|
+
agg = { ...f, key, count: 0, components: {}, reports: [] };
|
|
1293
1670
|
byKey.set(key, agg);
|
|
1294
1671
|
}
|
|
1295
1672
|
agg.count++;
|
|
1296
1673
|
agg.components[r.component] = (agg.components[r.component] || 0) + 1;
|
|
1674
|
+
if (agg.reports.length < MAX_FIX_REPORTS) agg.reports.push(r);
|
|
1297
1675
|
}
|
|
1298
1676
|
}
|
|
1299
1677
|
return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
|
|
@@ -1303,6 +1681,8 @@ function formatFixes(reports, limit = 10) {
|
|
|
1303
1681
|
if (!ranked.length) return "No avoidable re-renders.";
|
|
1304
1682
|
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(", ")})`);
|
|
1305
1683
|
if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
|
|
1684
|
+
const roots = formatRootCauses(reports, 5);
|
|
1685
|
+
if (roots) lines.push("", "Root causes:", roots);
|
|
1306
1686
|
return lines.join("\n");
|
|
1307
1687
|
}
|
|
1308
1688
|
|
|
@@ -1359,6 +1739,8 @@ function compareSummaries(before, after) {
|
|
|
1359
1739
|
regressions: rows.filter((r) => r.delta > 0)
|
|
1360
1740
|
};
|
|
1361
1741
|
}
|
|
1742
|
+
var summarizeSession = (session, reports) => summarizeReports(reports, session);
|
|
1743
|
+
var compareSessions = compareSummaries;
|
|
1362
1744
|
function formatComparison(c) {
|
|
1363
1745
|
const w = Math.max(14, ...c.rows.map((r) => r.component.length + 2));
|
|
1364
1746
|
const line = (label, b, a, d) => `${label.padEnd(w)} ${String(b).padStart(8)} ${String(a).padStart(8)} ${d.padStart(8)}`;
|
|
@@ -1557,7 +1939,7 @@ function flash(target, duration = 500) {
|
|
|
1557
1939
|
}
|
|
1558
1940
|
|
|
1559
1941
|
// src/version.ts
|
|
1560
|
-
var VERSION = "0.
|
|
1942
|
+
var VERSION = "0.5.0" ;
|
|
1561
1943
|
|
|
1562
1944
|
// src/devtools.ts
|
|
1563
1945
|
var DEVTOOLS_MARKER = "__rerenderLens";
|
|
@@ -1889,47 +2271,63 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1889
2271
|
};
|
|
1890
2272
|
}
|
|
1891
2273
|
|
|
2274
|
+
exports.AVOIDABLE_KINDS = AVOIDABLE_KINDS;
|
|
1892
2275
|
exports.DEFAULT_CHANNEL = DEFAULT_CHANNEL;
|
|
1893
2276
|
exports.DEVTOOLS_MARKER = DEVTOOLS_MARKER;
|
|
2277
|
+
exports.KIND_LABEL = KIND_LABEL;
|
|
1894
2278
|
exports.MARKER = MARKER;
|
|
1895
2279
|
exports.PROTOCOL_VERSION = PROTOCOL_VERSION;
|
|
1896
2280
|
exports.VERSION = VERSION;
|
|
2281
|
+
exports.analyzeCommit = analyzeCommit;
|
|
1897
2282
|
exports.assertWithinBudget = assertWithinBudget;
|
|
1898
2283
|
exports.avoidableCounts = avoidableCounts;
|
|
1899
2284
|
exports.buildReport = buildReport;
|
|
2285
|
+
exports.cascadeTree = cascadeTree;
|
|
1900
2286
|
exports.checkBudget = checkBudget;
|
|
1901
2287
|
exports.classify = classify;
|
|
1902
2288
|
exports.combineNotifiers = combineNotifiers;
|
|
2289
|
+
exports.compareSessions = compareSessions;
|
|
1903
2290
|
exports.compareSummaries = compareSummaries;
|
|
1904
2291
|
exports.configure = configure;
|
|
2292
|
+
exports.contextAttribution = contextAttribution;
|
|
1905
2293
|
exports.createCollector = createCollector;
|
|
1906
2294
|
exports.createDevtoolsNotifier = createDevtoolsNotifier;
|
|
1907
2295
|
exports.customHooksFromStack = customHooksFromStack;
|
|
1908
2296
|
exports.deepEqual = deepEqual;
|
|
1909
2297
|
exports.deserializeOptions = deserializeOptions;
|
|
2298
|
+
exports.diffLeaves = diffLeaves;
|
|
1910
2299
|
exports.diffRecords = diffRecords;
|
|
1911
2300
|
exports.disable = disable;
|
|
1912
2301
|
exports.ensureDevtoolsHook = ensureDevtoolsHook;
|
|
2302
|
+
exports.firstDifferentPath = firstDifferentPath;
|
|
1913
2303
|
exports.fixKey = fixKey;
|
|
1914
2304
|
exports.fixesFor = fixesFor;
|
|
1915
2305
|
exports.formatComparison = formatComparison;
|
|
1916
2306
|
exports.formatFixes = formatFixes;
|
|
2307
|
+
exports.formatRootCauses = formatRootCauses;
|
|
1917
2308
|
exports.getDisplayName = getDisplayName;
|
|
1918
2309
|
exports.getRenderers = getRenderers;
|
|
2310
|
+
exports.groupByCommit = groupByCommit;
|
|
1919
2311
|
exports.hookLabel = hookLabel;
|
|
2312
|
+
exports.indexByComponent = indexByComponent;
|
|
1920
2313
|
exports.init = init;
|
|
1921
2314
|
exports.isEnabled = isEnabled;
|
|
1922
2315
|
exports.isProductionReact = isProductionReact;
|
|
1923
2316
|
exports.parseExport = parseExport;
|
|
1924
2317
|
exports.printReport = printReport;
|
|
1925
2318
|
exports.rankFixes = rankFixes;
|
|
2319
|
+
exports.rankRootCauses = rankRootCauses;
|
|
1926
2320
|
exports.resolveHookNames = resolveHookNames;
|
|
2321
|
+
exports.rootCauseOf = rootCauseOf;
|
|
2322
|
+
exports.rootCauseSummary = rootCauseSummary;
|
|
1927
2323
|
exports.serialize = serialize;
|
|
1928
2324
|
exports.serializeOptions = serializeOptions;
|
|
2325
|
+
exports.shortValue = shortValue;
|
|
1929
2326
|
exports.shouldTrack = shouldTrack;
|
|
1930
2327
|
exports.storeAdvice = storeAdvice;
|
|
1931
2328
|
exports.summarize = summarize;
|
|
1932
2329
|
exports.summarizeReports = summarizeReports;
|
|
2330
|
+
exports.summarizeSession = summarizeSession;
|
|
1933
2331
|
exports.toBudget = toBudget;
|
|
1934
2332
|
exports.track = track;
|
|
1935
2333
|
exports.useWhyRerender = useWhyRerender;
|