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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* rerender-lens 0.
|
|
1
|
+
/* rerender-lens 0.5.0 — rerender-lens.core.js: types, version, diff, report, state, overlay. Built by scripts/build-vendor.mjs; do not edit. */
|
|
2
2
|
(() => {
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __export = (target, all) => {
|
|
@@ -18,16 +18,18 @@
|
|
|
18
18
|
__export(version_exports, {
|
|
19
19
|
VERSION: () => VERSION
|
|
20
20
|
});
|
|
21
|
-
var VERSION = true ? "0.
|
|
21
|
+
var VERSION = true ? "0.5.0" : "dev";
|
|
22
22
|
|
|
23
23
|
// src/diff.ts
|
|
24
24
|
var diff_exports = {};
|
|
25
25
|
__export(diff_exports, {
|
|
26
|
+
AVOIDABLE_KINDS: () => AVOIDABLE_KINDS,
|
|
26
27
|
DEFAULT_DIFF_BUDGET: () => DEFAULT_DIFF_BUDGET,
|
|
27
28
|
beginDiffScope: () => beginDiffScope,
|
|
28
29
|
classify: () => classify,
|
|
29
30
|
deepEqual: () => deepEqual,
|
|
30
31
|
diffBudgetExhausted: () => diffBudgetExhausted,
|
|
32
|
+
diffLeaves: () => diffLeaves,
|
|
31
33
|
diffRecords: () => diffRecords,
|
|
32
34
|
firstDifferentPath: () => firstDifferentPath,
|
|
33
35
|
isReactElement: () => isReactElement
|
|
@@ -153,6 +155,7 @@
|
|
|
153
155
|
}
|
|
154
156
|
return false;
|
|
155
157
|
}
|
|
158
|
+
var AVOIDABLE_KINDS = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
|
|
156
159
|
function classify(prev, next) {
|
|
157
160
|
if (typeof prev === "function" && typeof next === "function") {
|
|
158
161
|
return prev.name === next.name && prev.toString() === next.toString() ? "function" : "different";
|
|
@@ -189,35 +192,53 @@
|
|
|
189
192
|
if (Object.is(a, b)) continue;
|
|
190
193
|
const kind = classify(a, b);
|
|
191
194
|
if (kind === "different") {
|
|
192
|
-
changes.push({ path: diffBudgetExhausted() ? path : firstDifferentPath(a, b, path), kind, prev: a, next: b });
|
|
195
|
+
changes.push({ path: diffBudgetExhausted() ? path : firstDifferentPath(a, b, path) ?? path, kind, prev: a, next: b });
|
|
193
196
|
} else {
|
|
194
197
|
changes.push({ path, kind, prev: a, next: b });
|
|
195
198
|
}
|
|
196
199
|
}
|
|
197
200
|
return changes;
|
|
198
201
|
}
|
|
199
|
-
function firstDifferentPath(a, b, path, depth = 0) {
|
|
200
|
-
if (depth > 8) return path;
|
|
202
|
+
function firstDifferentPath(a, b, path = "", depth = 0) {
|
|
203
|
+
if (depth > 8) return path || "(value)";
|
|
201
204
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
202
|
-
if (a.length !== b.length) return `${path}.length
|
|
205
|
+
if (a.length !== b.length) return path ? `${path}.length` : "length";
|
|
203
206
|
for (let i = 0; i < a.length; i++) {
|
|
204
207
|
if (!deepEqual(a[i], b[i])) return firstDifferentPath(a[i], b[i], joinPath(path, i), depth + 1);
|
|
205
208
|
}
|
|
206
|
-
return
|
|
209
|
+
return null;
|
|
207
210
|
}
|
|
208
211
|
if (isPlainObject(a) && isPlainObject(b)) {
|
|
209
212
|
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
210
213
|
for (const k of keys) {
|
|
211
214
|
if (!deepEqual(a[k], b[k])) return firstDifferentPath(a[k], b[k], joinPath(path, k), depth + 1);
|
|
212
215
|
}
|
|
213
|
-
return
|
|
216
|
+
return null;
|
|
214
217
|
}
|
|
215
|
-
return path;
|
|
218
|
+
return deepEqual(a, b) ? null : path || "(value)";
|
|
219
|
+
}
|
|
220
|
+
function diffLeaves(a, b, limit = 20, path = "", out = []) {
|
|
221
|
+
if (out.length >= limit || Object.is(a, b)) return out;
|
|
222
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
223
|
+
const n = Math.max(a.length, b.length);
|
|
224
|
+
for (let i = 0; i < n && out.length < limit; i++) diffLeaves(a[i], b[i], limit, joinPath(path, i), out);
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
228
|
+
for (const k of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) {
|
|
229
|
+
if (out.length >= limit) break;
|
|
230
|
+
diffLeaves(a[k], b[k], limit, joinPath(path, k), out);
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
if (!deepEqual(a, b)) out.push({ path: path || "(value)", prev: a, next: b });
|
|
235
|
+
return out;
|
|
216
236
|
}
|
|
217
237
|
|
|
218
238
|
// src/report.ts
|
|
219
239
|
var report_exports = {};
|
|
220
240
|
__export(report_exports, {
|
|
241
|
+
KIND_LABEL: () => KIND_LABEL,
|
|
221
242
|
buildReport: () => buildReport,
|
|
222
243
|
fixFor: () => fixFor,
|
|
223
244
|
hookLabel: () => hookLabel,
|
|
@@ -239,10 +260,12 @@
|
|
|
239
260
|
}
|
|
240
261
|
var isGenuine = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
|
|
241
262
|
var isChildren = (change) => change.path === "children";
|
|
263
|
+
var isRefObject = (change) => change.path === "ref" && change.kind === "deep-equal";
|
|
242
264
|
function fixFor(change) {
|
|
243
265
|
if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
|
|
244
266
|
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`;
|
|
245
267
|
}
|
|
268
|
+
if (isRefObject(change)) return `create the ref once with useRef (or createRef outside the render) and pass the same object every time`;
|
|
246
269
|
switch (change.kind) {
|
|
247
270
|
case "function":
|
|
248
271
|
return `wrap it in useCallback (or hoist it out of the parent's render)`;
|
|
@@ -258,6 +281,7 @@
|
|
|
258
281
|
if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
|
|
259
282
|
return `children are new React elements with the same types and props on every render of the parent`;
|
|
260
283
|
}
|
|
284
|
+
if (isRefObject(change)) return `prop "ref" is a new ref object on every render (React re-attaches it each time)`;
|
|
261
285
|
switch (change.kind) {
|
|
262
286
|
case "deep-equal":
|
|
263
287
|
return `prop "${change.path}" is a new reference but deep-equal to the previous value`;
|
|
@@ -285,11 +309,21 @@
|
|
|
285
309
|
if (causes.length === 0) trigger = "parent";
|
|
286
310
|
else if (causes.length === 1) trigger = causes[0];
|
|
287
311
|
else trigger = "mixed";
|
|
288
|
-
const
|
|
312
|
+
const nothingChanged = input.propChanges.length === 0 && stateChanges.length === 0 && hookChanges.length === 0;
|
|
313
|
+
const cacheHit = input.compiled === true && trigger === "parent" && input.propChanges.length === 0;
|
|
314
|
+
const revealed = input.revealed === true && trigger === "parent";
|
|
315
|
+
const avoidable = trigger === "parent" && !cacheHit && !revealed;
|
|
289
316
|
const reasons = [];
|
|
290
|
-
|
|
317
|
+
const memoFix = input.classComponent ? `Make "${input.component}" extend PureComponent (or implement shouldComponentUpdate)` : `Wrap "${input.component}" in React.memo (or extend PureComponent)`;
|
|
318
|
+
if (cacheHit) {
|
|
319
|
+
reasons.push(`compiled by React Compiler: its output is memoized and the inputs did not change, so this render is cheap; nothing to fix.`);
|
|
320
|
+
} else if (revealed) {
|
|
321
|
+
reasons.push(
|
|
322
|
+
`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.`
|
|
323
|
+
);
|
|
324
|
+
} else if (avoidable && nothingChanged) {
|
|
291
325
|
const who = input.parent ? `<${input.parent.name}> re-rendered (${describeTrigger(input.parent.trigger)})` : "its parent re-rendered";
|
|
292
|
-
reasons.push(`re-rendered with identical props because ${who}.
|
|
326
|
+
reasons.push(`re-rendered with identical props because ${who}. ${memoFix}.`);
|
|
293
327
|
} else if (avoidable && input.parent) {
|
|
294
328
|
reasons.push(`caused by <${input.parent.name}> re-rendering (${describeTrigger(input.parent.trigger)}).`);
|
|
295
329
|
}
|
|
@@ -298,9 +332,9 @@
|
|
|
298
332
|
reasons.push(fix ? `${describe(c)}: ${fix}.` : `${describe(c)}.`);
|
|
299
333
|
}
|
|
300
334
|
const memoized = input.memoized !== false;
|
|
301
|
-
if (avoidable && !memoized && input.propChanges.length > 0) {
|
|
335
|
+
if (avoidable && !memoized && !input.compiled && input.propChanges.length > 0) {
|
|
302
336
|
reasons.push(
|
|
303
|
-
`"${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).`
|
|
337
|
+
`"${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)"}.`
|
|
304
338
|
);
|
|
305
339
|
}
|
|
306
340
|
for (const c of stateChanges) {
|
|
@@ -312,6 +346,8 @@
|
|
|
312
346
|
const where = c.provider && c.provider.component ? ` (provided by <${c.provider.component}>)` : "";
|
|
313
347
|
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` : "";
|
|
314
348
|
reasons.push(`${c.path} changed${where}${keys}.`);
|
|
349
|
+
} else if (c.hook === "useDeferredValue" && isGenuine(c)) {
|
|
350
|
+
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.`);
|
|
315
351
|
} else if (isGenuine(c)) reasons.push(`${hookLabel(c)} changed.`);
|
|
316
352
|
else if (isStateHook(c))
|
|
317
353
|
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.`);
|
|
@@ -342,6 +378,7 @@
|
|
|
342
378
|
reasons,
|
|
343
379
|
time: now()
|
|
344
380
|
};
|
|
381
|
+
if (input.compiled) report.compiled = true;
|
|
345
382
|
if (input.selfDuration !== void 0) report.selfDuration = input.selfDuration;
|
|
346
383
|
if (input.treeDuration !== void 0) report.treeDuration = input.treeDuration;
|
|
347
384
|
if (input.commitPriority) report.commitPriority = input.commitPriority;
|
|
@@ -379,10 +416,10 @@
|
|
|
379
416
|
};
|
|
380
417
|
function summarize(report) {
|
|
381
418
|
const counts = /* @__PURE__ */ new Map();
|
|
382
|
-
for (const c of [...report.propChanges, ...report.stateChanges, ...report.hookChanges]) {
|
|
419
|
+
for (const c of [...report.propChanges || [], ...report.stateChanges || [], ...report.hookChanges || []]) {
|
|
383
420
|
counts.set(c.kind, (counts.get(c.kind) ?? 0) + 1);
|
|
384
421
|
}
|
|
385
|
-
const parts2 = [...counts].map(([kind, n]) => `${n} ${KIND_LABEL[kind]}`);
|
|
422
|
+
const parts2 = [...counts].map(([kind, n]) => `${n} ${KIND_LABEL[kind] || kind}`);
|
|
386
423
|
if (parts2.length === 0) parts2.push("no changes");
|
|
387
424
|
return parts2.join(", ");
|
|
388
425
|
}
|
|
@@ -575,7 +612,7 @@
|
|
|
575
612
|
})();
|
|
576
613
|
|
|
577
614
|
;
|
|
578
|
-
/* rerender-lens 0.
|
|
615
|
+
/* rerender-lens 0.5.0 — rerender-lens.engine.js: fiber, tracker, hookNames. Built by scripts/build-vendor.mjs; do not edit. */
|
|
579
616
|
(() => {
|
|
580
617
|
var __create = Object.create;
|
|
581
618
|
var __defProp = Object.defineProperty;
|
|
@@ -660,6 +697,7 @@
|
|
|
660
697
|
getRenderers: () => getRenderers,
|
|
661
698
|
hostNodesOf: () => hostNodesOf,
|
|
662
699
|
instanceIdOf: () => instanceIdOf,
|
|
700
|
+
isCompiledFiber: () => isCompiledFiber,
|
|
663
701
|
isMemoizedFiber: () => isMemoizedFiber,
|
|
664
702
|
isProductionReact: () => isProductionReact,
|
|
665
703
|
nearestComponent: () => nearestComponent,
|
|
@@ -785,6 +823,7 @@
|
|
|
785
823
|
useEffectEvent: 1,
|
|
786
824
|
useCacheRefresh: 1
|
|
787
825
|
};
|
|
826
|
+
var BUILTIN_HOOKS = /* @__PURE__ */ new Set([...Object.keys(NODE_COUNT), "useContext", "useDebugValue", "use"]);
|
|
788
827
|
var noop = () => {
|
|
789
828
|
};
|
|
790
829
|
function customHooksFromStack(stack, componentName, marker) {
|
|
@@ -799,7 +838,7 @@
|
|
|
799
838
|
continue;
|
|
800
839
|
}
|
|
801
840
|
if (name === componentName || name.endsWith("." + componentName)) break;
|
|
802
|
-
if (/^use[A-Z0-9_]/.test(name)) names.push(name);
|
|
841
|
+
if (/^use[A-Z0-9_]/.test(name) && !BUILTIN_HOOKS.has(name)) names.push(name);
|
|
803
842
|
if (names.length > 8) break;
|
|
804
843
|
}
|
|
805
844
|
return names;
|
|
@@ -1101,6 +1140,11 @@
|
|
|
1101
1140
|
}
|
|
1102
1141
|
return false;
|
|
1103
1142
|
}
|
|
1143
|
+
function isCompiledFiber(fiber) {
|
|
1144
|
+
if (fiber.tag === ClassComponent) return false;
|
|
1145
|
+
const cache2 = fiber.updateQueue?.memoCache;
|
|
1146
|
+
return !!cache2 && Array.isArray(cache2.data);
|
|
1147
|
+
}
|
|
1104
1148
|
function durationsOf(fiber) {
|
|
1105
1149
|
if (typeof fiber.actualDuration !== "number") return null;
|
|
1106
1150
|
const tree = fiber.actualDuration;
|
|
@@ -1279,7 +1323,8 @@
|
|
|
1279
1323
|
const labels = hookLabelsFor(fiber, b);
|
|
1280
1324
|
let i = 0;
|
|
1281
1325
|
while (a && b) {
|
|
1282
|
-
|
|
1326
|
+
const deferred = labels?.[i] === "useDeferredValue";
|
|
1327
|
+
if ((deferred || isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
|
|
1283
1328
|
const hook = labels?.[i] ?? hookLabel(b);
|
|
1284
1329
|
out.push({
|
|
1285
1330
|
path: `${hook}#${i}`,
|
|
@@ -1313,6 +1358,10 @@
|
|
|
1313
1358
|
let a = alt.dependencies?.firstContext ?? null;
|
|
1314
1359
|
let b = fiber.dependencies?.firstContext ?? null;
|
|
1315
1360
|
let i = 0;
|
|
1361
|
+
if (b && !("memoizedValue" in b)) {
|
|
1362
|
+
(0, import_state2.warnOnce)("contextValues", "this React does not record context values on the fiber (React < 18): context changes are reported as plain parent re-renders.");
|
|
1363
|
+
return out;
|
|
1364
|
+
}
|
|
1316
1365
|
while (a && b) {
|
|
1317
1366
|
if (a.context === b.context && !Object.is(a.memoizedValue, b.memoizedValue)) {
|
|
1318
1367
|
const name = b.context.displayName ?? "Context";
|
|
@@ -1345,10 +1394,28 @@
|
|
|
1345
1394
|
return out;
|
|
1346
1395
|
}
|
|
1347
1396
|
var isGenuine = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
|
|
1397
|
+
var isRefObject = (v) => {
|
|
1398
|
+
if (typeof v !== "object" || v === null) return false;
|
|
1399
|
+
const keys = Object.keys(v);
|
|
1400
|
+
return keys.length === 1 && keys[0] === "current";
|
|
1401
|
+
};
|
|
1402
|
+
function diffProps(prev, next) {
|
|
1403
|
+
if (prev === next) return [];
|
|
1404
|
+
const pr = prev.ref;
|
|
1405
|
+
const nr = next.ref;
|
|
1406
|
+
if (pr !== nr && isRefObject(pr) && isRefObject(nr)) {
|
|
1407
|
+
const { ref: _p, ...restPrev } = prev;
|
|
1408
|
+
const { ref: _n, ...restNext } = next;
|
|
1409
|
+
void _p;
|
|
1410
|
+
void _n;
|
|
1411
|
+
return [{ path: "ref", kind: "deep-equal", prev: pr, next: nr }, ...(0, import_diff.diffRecords)(restPrev, restNext)];
|
|
1412
|
+
}
|
|
1413
|
+
return (0, import_diff.diffRecords)(prev, next);
|
|
1414
|
+
}
|
|
1348
1415
|
function analyze(fiber, alt, trackHooks) {
|
|
1349
1416
|
const prevProps = alt.memoizedProps ?? {};
|
|
1350
1417
|
const nextProps = fiber.memoizedProps ?? {};
|
|
1351
|
-
const propChanges =
|
|
1418
|
+
const propChanges = diffProps(prevProps, nextProps);
|
|
1352
1419
|
let stateChanges = [];
|
|
1353
1420
|
let hookChanges = [];
|
|
1354
1421
|
if (fiber.tag === ClassComponent) {
|
|
@@ -1361,7 +1428,7 @@
|
|
|
1361
1428
|
} else if (trackHooks) {
|
|
1362
1429
|
hookChanges = [...diffHooks(fiber, alt), ...diffContexts(fiber, alt)];
|
|
1363
1430
|
}
|
|
1364
|
-
const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore";
|
|
1431
|
+
const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore" && h.hook !== "useDeferredValue";
|
|
1365
1432
|
const causes = [];
|
|
1366
1433
|
if (propChanges.some(isGenuine)) causes.push("props");
|
|
1367
1434
|
if (stateChanges.some(isGenuine) || hookChanges.some((h) => isStateHook(h) && isGenuine(h))) causes.push("state");
|
|
@@ -1401,6 +1468,24 @@
|
|
|
1401
1468
|
var EFFECT_LOOP_WINDOW_MS = 50;
|
|
1402
1469
|
var MAX_REPORTS_PER_COMMIT = 200;
|
|
1403
1470
|
var COMMIT_TIME_BUDGET_MS = 25;
|
|
1471
|
+
function isDeferredCatchUp(fiber) {
|
|
1472
|
+
const types = fiber._debugHookTypes;
|
|
1473
|
+
if (!types || !types.includes("useDeferredValue") || !fiber.alternate) return false;
|
|
1474
|
+
const first = fiber.memoizedState;
|
|
1475
|
+
if (!isHookList(first)) return false;
|
|
1476
|
+
const labels = hookLabelsFor(fiber, first);
|
|
1477
|
+
if (!labels) return false;
|
|
1478
|
+
let a = fiber.alternate.memoizedState;
|
|
1479
|
+
let b = first;
|
|
1480
|
+
for (let i = 0; a && b; a = a.next, b = b.next, i++) {
|
|
1481
|
+
if (labels[i] === "useDeferredValue" && !Object.is(a.memoizedState, b.memoizedState)) return true;
|
|
1482
|
+
}
|
|
1483
|
+
return false;
|
|
1484
|
+
}
|
|
1485
|
+
function isUnder(fiber, boundaries) {
|
|
1486
|
+
for (let f = fiber.return; f && f.tag !== HostRoot; f = f.return) if (f.tag === SuspenseComponent && boundaries.includes(f)) return true;
|
|
1487
|
+
return false;
|
|
1488
|
+
}
|
|
1404
1489
|
function onCommit(root, commitPriority) {
|
|
1405
1490
|
const s = (0, import_state2.getState)();
|
|
1406
1491
|
if (!s.enabled) return;
|
|
@@ -1410,7 +1495,7 @@
|
|
|
1410
1495
|
const rendered = [];
|
|
1411
1496
|
const renderedNames = /* @__PURE__ */ new Set();
|
|
1412
1497
|
let hot = false;
|
|
1413
|
-
let
|
|
1498
|
+
let resolvedBoundaries = null;
|
|
1414
1499
|
const stack = [root.current];
|
|
1415
1500
|
while (stack.length) {
|
|
1416
1501
|
const fiber = stack.pop();
|
|
@@ -1420,7 +1505,7 @@
|
|
|
1420
1505
|
renderedNames.add(fiberName(fiber));
|
|
1421
1506
|
if (shouldTrack(fiberType(fiber), o)) rendered.push(fiber);
|
|
1422
1507
|
}
|
|
1423
|
-
if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null)
|
|
1508
|
+
if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) (resolvedBoundaries ?? (resolvedBoundaries = [])).push(fiber);
|
|
1424
1509
|
if (!alt || fiber.child !== alt.child) {
|
|
1425
1510
|
for (let c = fiber.child; c; c = c.sibling) stack.push(c);
|
|
1426
1511
|
}
|
|
@@ -1432,10 +1517,10 @@
|
|
|
1432
1517
|
let commitCause;
|
|
1433
1518
|
let afterCommit;
|
|
1434
1519
|
const inputDriven = commitPriority === "immediate" || commitPriority === "user-blocking";
|
|
1435
|
-
if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u))) {
|
|
1520
|
+
if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u)) && !rendered.some(isDeferredCatchUp)) {
|
|
1436
1521
|
commitCause = "effect-after-commit";
|
|
1437
1522
|
afterCommit = lastCommit.id;
|
|
1438
|
-
} else if (
|
|
1523
|
+
} else if (resolvedBoundaries) commitCause = "suspense-resolved";
|
|
1439
1524
|
if (rendered.length === 0) {
|
|
1440
1525
|
lastCommit = { id: s.nextCommitId, at, rendered: renderedNames };
|
|
1441
1526
|
return;
|
|
@@ -1487,6 +1572,9 @@
|
|
|
1487
1572
|
owner: ownerName(fiber),
|
|
1488
1573
|
path: componentPath(fiber),
|
|
1489
1574
|
memoized: isMemoizedFiber(fiber),
|
|
1575
|
+
compiled: isCompiledFiber(fiber),
|
|
1576
|
+
classComponent: fiber.tag === ClassComponent,
|
|
1577
|
+
revealed: resolvedBoundaries !== null && a.trigger === "parent" && isUnder(fiber, resolvedBoundaries),
|
|
1490
1578
|
selfDuration: durations ? durations.self : void 0,
|
|
1491
1579
|
treeDuration: durations ? durations.tree : void 0,
|
|
1492
1580
|
commitId,
|
|
@@ -1508,7 +1596,7 @@
|
|
|
1508
1596
|
})();
|
|
1509
1597
|
|
|
1510
1598
|
;
|
|
1511
|
-
/* rerender-lens 0.
|
|
1599
|
+
/* rerender-lens 0.5.0 — rerender-lens.js: devtools bridge + notifiers (window.RerenderLens). Built by scripts/build-vendor.mjs; do not edit. */
|
|
1512
1600
|
"use strict";
|
|
1513
1601
|
var RerenderLens = (() => {
|
|
1514
1602
|
var __create = Object.create;
|
|
@@ -1621,14 +1709,14 @@ var RerenderLens = (() => {
|
|
|
1621
1709
|
MARKER: () => import_types.MARKER,
|
|
1622
1710
|
PROTOCOL_VERSION: () => PROTOCOL_VERSION,
|
|
1623
1711
|
VERSION: () => import_version2.VERSION,
|
|
1624
|
-
buildReport: () =>
|
|
1625
|
-
classify: () =>
|
|
1712
|
+
buildReport: () => import_report.buildReport,
|
|
1713
|
+
classify: () => import_diff4.classify,
|
|
1626
1714
|
combineNotifiers: () => combineNotifiers,
|
|
1627
1715
|
configure: () => import_tracker2.configure,
|
|
1628
1716
|
createCollector: () => createCollector,
|
|
1629
1717
|
createDevtoolsNotifier: () => createDevtoolsNotifier,
|
|
1630
|
-
deepEqual: () =>
|
|
1631
|
-
diffRecords: () =>
|
|
1718
|
+
deepEqual: () => import_diff4.deepEqual,
|
|
1719
|
+
diffRecords: () => import_diff4.diffRecords,
|
|
1632
1720
|
disable: () => import_tracker2.disable,
|
|
1633
1721
|
ensureDevtoolsHook: () => import_fiber2.ensureDevtoolsHook,
|
|
1634
1722
|
getDisplayName: () => import_tracker2.getDisplayName,
|
|
@@ -1636,51 +1724,219 @@ var RerenderLens = (() => {
|
|
|
1636
1724
|
init: () => import_tracker2.init,
|
|
1637
1725
|
isEnabled: () => import_tracker2.isEnabled,
|
|
1638
1726
|
isProductionReact: () => import_fiber2.isProductionReact,
|
|
1639
|
-
printReport: () =>
|
|
1727
|
+
printReport: () => import_report.printReport,
|
|
1640
1728
|
serialize: () => serialize,
|
|
1641
1729
|
shouldTrack: () => import_tracker2.shouldTrack,
|
|
1642
|
-
summarize: () =>
|
|
1730
|
+
summarize: () => import_report.summarize,
|
|
1643
1731
|
track: () => import_tracker2.track
|
|
1644
1732
|
});
|
|
1645
1733
|
var import_types = __toESM(require_types(), 1);
|
|
1646
1734
|
var import_tracker2 = __toESM(require_tracker(), 1);
|
|
1647
1735
|
var import_fiber2 = __toESM(require_fiber(), 1);
|
|
1736
|
+
var import_diff4 = __toESM(require_diff(), 1);
|
|
1737
|
+
var import_report = __toESM(require_report(), 1);
|
|
1738
|
+
|
|
1739
|
+
// src/fixes.ts
|
|
1648
1740
|
var import_diff2 = __toESM(require_diff(), 1);
|
|
1649
|
-
|
|
1741
|
+
|
|
1742
|
+
// src/causes.ts
|
|
1743
|
+
var import_diff = __toESM(require_diff(), 1);
|
|
1744
|
+
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);
|
|
1745
|
+
var bump = (m, key, n = 1) => void m.set(key, (m.get(key) || 0) + n);
|
|
1746
|
+
function indexByComponent(reports) {
|
|
1747
|
+
const index = /* @__PURE__ */ new Map();
|
|
1748
|
+
for (const r of reports) {
|
|
1749
|
+
const list = index.get(r.component);
|
|
1750
|
+
if (list) list.push(r);
|
|
1751
|
+
else index.set(r.component, [r]);
|
|
1752
|
+
}
|
|
1753
|
+
return index;
|
|
1754
|
+
}
|
|
1755
|
+
function rootCauseOf(r, commitReports, index = indexByComponent(commitReports)) {
|
|
1756
|
+
let cur = r;
|
|
1757
|
+
const seen = /* @__PURE__ */ new Set([r]);
|
|
1758
|
+
while (cur.trigger === "parent" && cur.parent) {
|
|
1759
|
+
const parent = cur.parent;
|
|
1760
|
+
const candidates = index.get(parent.name);
|
|
1761
|
+
const p = candidates && candidates.find((x) => isAncestorReport(x, cur));
|
|
1762
|
+
if (!p || seen.has(p)) return { name: parent.name, trigger: parent.trigger, report: null };
|
|
1763
|
+
seen.add(p);
|
|
1764
|
+
cur = p;
|
|
1765
|
+
}
|
|
1766
|
+
return cur === r ? null : { name: cur.component, trigger: cur.trigger, report: cur };
|
|
1767
|
+
}
|
|
1768
|
+
function rootCausesOf(reports) {
|
|
1769
|
+
const roots = /* @__PURE__ */ new Map();
|
|
1770
|
+
const rootByReport = /* @__PURE__ */ new Map();
|
|
1771
|
+
const index = indexByComponent(reports);
|
|
1772
|
+
let avoidable = 0;
|
|
1773
|
+
let wasted = 0;
|
|
1774
|
+
for (const r of reports) {
|
|
1775
|
+
if (!r.avoidable) continue;
|
|
1776
|
+
avoidable++;
|
|
1777
|
+
if (typeof r.selfDuration === "number") wasted += r.selfDuration;
|
|
1778
|
+
const root = rootCauseOf(r, reports, index);
|
|
1779
|
+
const name = root ? root.name : r.parent && r.parent.name || "(unknown)";
|
|
1780
|
+
const trigger = root ? root.trigger : r.parent && r.parent.trigger || "parent";
|
|
1781
|
+
rootByReport.set(r, name);
|
|
1782
|
+
let agg = roots.get(name);
|
|
1783
|
+
if (!agg) {
|
|
1784
|
+
agg = { name, trigger, count: 0, components: /* @__PURE__ */ new Map() };
|
|
1785
|
+
roots.set(name, agg);
|
|
1786
|
+
}
|
|
1787
|
+
agg.count++;
|
|
1788
|
+
bump(agg.components, r.component);
|
|
1789
|
+
}
|
|
1790
|
+
return { roots: [...roots.values()].sort((a, b) => b.count - a.count), rootByReport, avoidable, wasted };
|
|
1791
|
+
}
|
|
1792
|
+
function groupByCommit(reports) {
|
|
1793
|
+
const out = /* @__PURE__ */ new Map();
|
|
1794
|
+
for (const r of reports) {
|
|
1795
|
+
if (typeof r.commitId !== "number" || r.commitId <= 0) continue;
|
|
1796
|
+
const list = out.get(r.commitId);
|
|
1797
|
+
if (list) list.push(r);
|
|
1798
|
+
else out.set(r.commitId, [r]);
|
|
1799
|
+
}
|
|
1800
|
+
return out;
|
|
1801
|
+
}
|
|
1802
|
+
function rankRootCauses(reports) {
|
|
1803
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1804
|
+
for (const commit of groupByCommit(reports).values()) {
|
|
1805
|
+
for (const root of rootCausesOf(commit).roots) {
|
|
1806
|
+
let agg = byName.get(root.name);
|
|
1807
|
+
if (!agg) {
|
|
1808
|
+
agg = { name: root.name, trigger: root.trigger, commits: 0, count: 0, components: /* @__PURE__ */ new Map() };
|
|
1809
|
+
byName.set(root.name, agg);
|
|
1810
|
+
}
|
|
1811
|
+
agg.commits++;
|
|
1812
|
+
agg.count += root.count;
|
|
1813
|
+
for (const [c, n] of root.components) bump(agg.components, c, n);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
return [...byName.values()].sort((a, b) => b.count - a.count || b.commits - a.commits || a.name.localeCompare(b.name));
|
|
1817
|
+
}
|
|
1818
|
+
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
1819
|
+
function formatRootCauses(reports, limit = 5) {
|
|
1820
|
+
const ranked = rankRootCauses(reports);
|
|
1821
|
+
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(", ")})`);
|
|
1822
|
+
if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
|
|
1823
|
+
return lines.join("\n");
|
|
1824
|
+
}
|
|
1650
1825
|
|
|
1651
1826
|
// src/fixes.ts
|
|
1652
|
-
var
|
|
1653
|
-
var AVOIDABLE = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
|
|
1827
|
+
var MAX_FIX_REPORTS = 50;
|
|
1654
1828
|
var rootOf = (path) => path.split(/[.[]/)[0] || path;
|
|
1829
|
+
var identifier = (name) => /^[A-Za-z_$][\w$]*$/.test(name) ? name : "value";
|
|
1830
|
+
function shortValue(v, max = 60) {
|
|
1831
|
+
let s;
|
|
1832
|
+
try {
|
|
1833
|
+
s = JSON.stringify(v);
|
|
1834
|
+
} catch {
|
|
1835
|
+
s = String(v);
|
|
1836
|
+
}
|
|
1837
|
+
if (s === void 0) s = String(v);
|
|
1838
|
+
return s.length > max ? s.slice(0, max - 3) + "..." : s;
|
|
1839
|
+
}
|
|
1655
1840
|
function fixesFor(r) {
|
|
1656
1841
|
const out = [];
|
|
1657
|
-
const
|
|
1658
|
-
const
|
|
1842
|
+
const ownerName = r.owner || r.parent && r.parent.name || null;
|
|
1843
|
+
const propChanges = r.propChanges || [];
|
|
1844
|
+
const changes = [...propChanges, ...r.stateChanges || [], ...r.hookChanges || []];
|
|
1659
1845
|
if (r.avoidable && (changes.length === 0 || r.memoized === false)) {
|
|
1846
|
+
const identical = changes.length === 0;
|
|
1660
1847
|
out.push({
|
|
1661
1848
|
kind: "memo",
|
|
1662
1849
|
owner: r.component,
|
|
1663
1850
|
target: r.component,
|
|
1664
1851
|
prop: null,
|
|
1665
1852
|
label: `Wrap <${r.component}> in React.memo`,
|
|
1666
|
-
detail:
|
|
1853
|
+
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.`,
|
|
1854
|
+
snippet: `// ${r.component}
|
|
1855
|
+
import { memo } from 'react';
|
|
1856
|
+
|
|
1857
|
+
export const ${r.component} = memo(function ${r.component}(props) {
|
|
1858
|
+
// ...
|
|
1859
|
+
});
|
|
1860
|
+
// class components: extend PureComponent instead`
|
|
1667
1861
|
});
|
|
1668
1862
|
}
|
|
1669
|
-
for (const c of
|
|
1670
|
-
if (!
|
|
1863
|
+
for (const c of propChanges) {
|
|
1864
|
+
if (!import_diff2.AVOIDABLE_KINDS.has(c.kind)) continue;
|
|
1865
|
+
const owner = ownerName || "?";
|
|
1671
1866
|
const root = rootOf(c.path);
|
|
1867
|
+
const id = identifier(root);
|
|
1672
1868
|
if (root === "children" && (c.kind === "element" || c.kind === "deep-equal")) {
|
|
1673
|
-
out.push({
|
|
1869
|
+
out.push({
|
|
1870
|
+
kind: "children",
|
|
1871
|
+
owner,
|
|
1872
|
+
target: r.component,
|
|
1873
|
+
prop: "children",
|
|
1874
|
+
label: `memoize children of <${r.component}> in <${owner}>`,
|
|
1875
|
+
detail: `<${owner}> re-creates the children of <${r.component}> on every render; they have the same types and props each time.`,
|
|
1876
|
+
snippet: `// ${owner}
|
|
1877
|
+
import { useMemo } from 'react';
|
|
1878
|
+
|
|
1879
|
+
const children = useMemo(() => (
|
|
1880
|
+
<>{/* the same elements */}</>
|
|
1881
|
+
), [/* deps */]);
|
|
1882
|
+
|
|
1883
|
+
<${r.component}>{children}</${r.component}>
|
|
1884
|
+
|
|
1885
|
+
// static children: hoist them to module scope
|
|
1886
|
+
const STATIC = <em>hi</em>;`
|
|
1887
|
+
});
|
|
1674
1888
|
} else if (c.kind === "function") {
|
|
1675
|
-
out.push({
|
|
1889
|
+
out.push({
|
|
1890
|
+
kind: "useCallback",
|
|
1891
|
+
owner,
|
|
1892
|
+
target: r.component,
|
|
1893
|
+
prop: root,
|
|
1894
|
+
label: `useCallback(${root}) in <${owner}>`,
|
|
1895
|
+
detail: `prop "${c.path}" of <${r.component}> is a new function on every render of <${owner}>.`,
|
|
1896
|
+
snippet: `// ${owner}
|
|
1897
|
+
import { useCallback } from 'react';
|
|
1898
|
+
|
|
1899
|
+
const ${id} = useCallback((/* args */) => {
|
|
1900
|
+
// ...
|
|
1901
|
+
}, [/* deps */]);
|
|
1902
|
+
|
|
1903
|
+
<${r.component} ${root}={${id}} />`
|
|
1904
|
+
});
|
|
1676
1905
|
} else if (c.kind === "element") {
|
|
1677
|
-
out.push({
|
|
1906
|
+
out.push({
|
|
1907
|
+
kind: "useMemoElement",
|
|
1908
|
+
owner,
|
|
1909
|
+
target: r.component,
|
|
1910
|
+
prop: root,
|
|
1911
|
+
label: `memoize element prop ${root} in <${owner}>`,
|
|
1912
|
+
detail: `prop "${c.path}" of <${r.component}> is a new element with the same type and props on every render of <${owner}>.`,
|
|
1913
|
+
snippet: `// ${owner}
|
|
1914
|
+
import { useMemo } from 'react';
|
|
1915
|
+
|
|
1916
|
+
const ${id} = useMemo(() => ${shortValue(c.next, 40)}, [/* deps */]);
|
|
1917
|
+
// or pass it as children from a component that does not re-render`
|
|
1918
|
+
});
|
|
1678
1919
|
} else {
|
|
1679
|
-
|
|
1920
|
+
const isArray = Array.isArray(c.next);
|
|
1921
|
+
out.push({
|
|
1922
|
+
kind: "useMemo",
|
|
1923
|
+
owner,
|
|
1924
|
+
target: r.component,
|
|
1925
|
+
prop: root,
|
|
1926
|
+
label: `useMemo(${root}) in <${owner}>`,
|
|
1927
|
+
detail: `prop "${c.path}" of <${r.component}> is a new ${isArray ? "array" : "object"} with the same contents on every render of <${owner}>.`,
|
|
1928
|
+
snippet: `// ${owner}
|
|
1929
|
+
import { useMemo } from 'react';
|
|
1930
|
+
|
|
1931
|
+
const ${id} = useMemo(() => (${shortValue(c.next, 80)}), [/* deps */]);
|
|
1932
|
+
|
|
1933
|
+
// or, when it never changes, hoist it to module scope:
|
|
1934
|
+
const ${id.toUpperCase()} = ${shortValue(c.next, 80)};`
|
|
1935
|
+
});
|
|
1680
1936
|
}
|
|
1681
1937
|
}
|
|
1682
|
-
for (const c of [...r.stateChanges, ...r.hookChanges]) {
|
|
1683
|
-
const isContext = c.hook === "useContext";
|
|
1938
|
+
for (const c of [...r.stateChanges || [], ...r.hookChanges || []]) {
|
|
1939
|
+
const isContext = c.hook === "useContext" || /^useContext/.test(c.path);
|
|
1684
1940
|
const ctxName = isContext ? (/useContext\((.*)\)/.exec(c.path) || [])[1] || "Context" : "";
|
|
1685
1941
|
const providerOwner = isContext && c.provider && c.provider.component ? c.provider.component : null;
|
|
1686
1942
|
if (isContext && c.kind === "different" && c.changedKeys && typeof c.totalKeys === "number" && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys) {
|
|
@@ -1690,20 +1946,63 @@ var RerenderLens = (() => {
|
|
|
1690
1946
|
target: r.component,
|
|
1691
1947
|
prop: ctxName,
|
|
1692
1948
|
label: `split ${ctxName}${providerOwner ? ` in <${providerOwner}>` : ""}: only ${c.changedKeys.join(", ")} changed`,
|
|
1693
|
-
detail: `${c.changedKeys.
|
|
1949
|
+
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.`,
|
|
1950
|
+
snippet: `// ${providerOwner || "Provider"}
|
|
1951
|
+
// one context per independently-changing slice
|
|
1952
|
+
const ${identifier(ctxName)}Static = createContext(...);
|
|
1953
|
+
const ${identifier(ctxName)}${c.changedKeys.map((k) => k[0].toUpperCase() + k.slice(1)).join("")} = createContext(...);
|
|
1954
|
+
|
|
1955
|
+
// or keep one context and let consumers select a slice:
|
|
1956
|
+
const ${c.changedKeys[0]} = useContextSelector(${ctxName}, (v) => v.${c.changedKeys[0]});`
|
|
1694
1957
|
});
|
|
1695
1958
|
continue;
|
|
1696
1959
|
}
|
|
1697
|
-
if (!
|
|
1960
|
+
if (!import_diff2.AVOIDABLE_KINDS.has(c.kind)) continue;
|
|
1698
1961
|
if (isContext) {
|
|
1699
|
-
out.push({
|
|
1962
|
+
out.push({
|
|
1963
|
+
kind: "contextValue",
|
|
1964
|
+
owner: providerOwner || `${ctxName}.Provider`,
|
|
1965
|
+
target: r.component,
|
|
1966
|
+
prop: ctxName,
|
|
1967
|
+
label: `memoize the ${ctxName} provider value${providerOwner ? ` in <${providerOwner}>` : ""}`,
|
|
1968
|
+
detail: `<${r.component}> re-rendered because ${ctxName} produced a new value that is deep-equal to the previous one${providerOwner ? ` (Provider rendered by <${providerOwner}>)` : ""}.`,
|
|
1969
|
+
snippet: `// ${providerOwner || `where <${ctxName}.Provider> is rendered`}
|
|
1970
|
+
const value = useMemo(() => ({ /* ... */ }), [/* deps */]);
|
|
1971
|
+
<${ctxName}.Provider value={value}>`
|
|
1972
|
+
});
|
|
1700
1973
|
} else if (c.hook === "useSyncExternalStore") {
|
|
1701
1974
|
const chain = c.custom || [];
|
|
1702
1975
|
const redux = chain.some((n) => /^use(App)?Selector$/.test(n));
|
|
1703
1976
|
const zustand = !redux && chain.some((n) => /^use[A-Z]\w*Store$/.test(n) || n === "useStore" || n === "useBoundStore");
|
|
1704
|
-
|
|
1977
|
+
const via = chain.length ? ` via ${chain.join(" \u203A ")}` : "";
|
|
1978
|
+
out.push({
|
|
1979
|
+
kind: "storeSnapshot",
|
|
1980
|
+
owner: r.component,
|
|
1981
|
+
target: r.component,
|
|
1982
|
+
prop: c.path,
|
|
1983
|
+
label: redux ? `memoize the selector in <${r.component}>` : zustand ? `useShallow in <${r.component}>` : `stable getSnapshot in <${r.component}>`,
|
|
1984
|
+
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.`,
|
|
1985
|
+
snippet: redux ? `// ${r.component}
|
|
1986
|
+
import { shallowEqual } from 'react-redux';
|
|
1987
|
+
const slice = useSelector(selectSlice, shallowEqual);
|
|
1988
|
+
// or memoize: const selectSlice = createSelector([selectA, selectB], (a, b) => ({ a, b }));` : zustand ? `// ${r.component}
|
|
1989
|
+
import { useShallow } from 'zustand/react/shallow';
|
|
1990
|
+
const { a, b } = useStore(useShallow((s) => ({ a: s.a, b: s.b })));
|
|
1991
|
+
// or select a primitive: const a = useStore((s) => s.a);` : `// ${r.component}
|
|
1992
|
+
// getSnapshot must return the same reference while the data is unchanged
|
|
1993
|
+
const snapshot = useSyncExternalStore(subscribe, store.getSnapshot /* cached */);`
|
|
1994
|
+
});
|
|
1705
1995
|
} else {
|
|
1706
|
-
out.push({
|
|
1996
|
+
out.push({
|
|
1997
|
+
kind: "bailout",
|
|
1998
|
+
owner: r.component,
|
|
1999
|
+
target: r.component,
|
|
2000
|
+
prop: c.path,
|
|
2001
|
+
label: `bail out before setting ${c.path} in <${r.component}>`,
|
|
2002
|
+
detail: `${c.path} was set to a value deep-equal to the current one (new reference).`,
|
|
2003
|
+
snippet: `// ${r.component}
|
|
2004
|
+
setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
2005
|
+
});
|
|
1707
2006
|
}
|
|
1708
2007
|
}
|
|
1709
2008
|
return out;
|
|
@@ -1717,11 +2016,12 @@ var RerenderLens = (() => {
|
|
|
1717
2016
|
const key = fixKey(f);
|
|
1718
2017
|
let agg = byKey.get(key);
|
|
1719
2018
|
if (!agg) {
|
|
1720
|
-
agg = { ...f, key, count: 0, components: {} };
|
|
2019
|
+
agg = { ...f, key, count: 0, components: {}, reports: [] };
|
|
1721
2020
|
byKey.set(key, agg);
|
|
1722
2021
|
}
|
|
1723
2022
|
agg.count++;
|
|
1724
2023
|
agg.components[r.component] = (agg.components[r.component] || 0) + 1;
|
|
2024
|
+
if (agg.reports.length < MAX_FIX_REPORTS) agg.reports.push(r);
|
|
1725
2025
|
}
|
|
1726
2026
|
}
|
|
1727
2027
|
return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
|
|
@@ -1731,6 +2031,8 @@ var RerenderLens = (() => {
|
|
|
1731
2031
|
if (!ranked.length) return "No avoidable re-renders.";
|
|
1732
2032
|
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(", ")})`);
|
|
1733
2033
|
if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
|
|
2034
|
+
const roots = formatRootCauses(reports, 5);
|
|
2035
|
+
if (roots) lines.push("", "Root causes:", roots);
|
|
1734
2036
|
return lines.join("\n");
|
|
1735
2037
|
}
|
|
1736
2038
|
|
|
@@ -1836,7 +2138,7 @@ ${formatFixes(reports)}`);
|
|
|
1836
2138
|
}
|
|
1837
2139
|
|
|
1838
2140
|
// src/devtools.ts
|
|
1839
|
-
var
|
|
2141
|
+
var import_diff3 = __toESM(require_diff(), 1);
|
|
1840
2142
|
var import_tracker = __toESM(require_tracker(), 1);
|
|
1841
2143
|
var import_state = __toESM(require_state(), 1);
|
|
1842
2144
|
var import_fiber = __toESM(require_fiber(), 1);
|
|
@@ -1862,7 +2164,7 @@ ${formatFixes(reports)}`);
|
|
|
1862
2164
|
const next = (v) => serialize(v, maxDepth, seen, depth + 1, budget);
|
|
1863
2165
|
seen.add(obj);
|
|
1864
2166
|
try {
|
|
1865
|
-
if ((0,
|
|
2167
|
+
if ((0, import_diff3.isReactElement)(obj)) {
|
|
1866
2168
|
const out2 = { $type: "element", name: (0, import_tracker.getDisplayName)(obj.type) };
|
|
1867
2169
|
if (obj.key !== null && obj.key !== void 0) out2.key = String(obj.key);
|
|
1868
2170
|
out2.props = next(obj.props);
|