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