rerender-lens 0.4.0 → 0.7.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 +127 -0
- package/README.md +208 -359
- package/dist/{budget-LkNjGtRc.d.ts → budget-CA1MS7lB.d.cts} +20 -10
- package/dist/{budget-COBu7jBU.d.cts → budget-ok1VDJ4t.d.ts} +20 -10
- package/dist/cli.cjs +322 -53
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +322 -53
- package/dist/cli.js.map +1 -1
- package/dist/{devtools-BkCct3cJ.d.cts → devtools-Cl5n_52v.d.ts} +17 -2
- package/dist/{devtools-CZebzpF6.d.ts → devtools-UwQx2VYS.d.cts} +17 -2
- package/dist/index.cjs +537 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +137 -8
- package/dist/index.d.ts +137 -8
- package/dist/index.js +521 -50
- package/dist/index.js.map +1 -1
- package/dist/jest-setup.cjs +1690 -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 +1688 -0
- package/dist/jest-setup.js.map +1 -0
- package/dist/jest.cjs +1777 -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 +1768 -0
- package/dist/jest.js.map +1 -0
- package/dist/{notifiers-BjjGSHpp.d.ts → notifiers-B5poAEvE.d.cts} +8 -4
- package/dist/{notifiers-BGQtWKfX.d.cts → notifiers-CqQW0Eum.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 +71 -19
- package/dist/relay.cjs.map +1 -1
- package/dist/relay.d.cts +25 -3
- package/dist/relay.d.ts +25 -3
- package/dist/relay.js +71 -19
- package/dist/relay.js.map +1 -1
- package/dist/rerender-lens.iife.js +457 -62
- package/dist/runner-report-Bvqoe89M.d.ts +46 -0
- package/dist/runner-report-CvpDg2zt.d.cts +46 -0
- package/dist/setup.cjs +209 -35
- package/dist/setup.cjs.map +1 -1
- package/dist/setup.js +209 -35
- package/dist/setup.js.map +1 -1
- package/dist/{types-BzUEVkxJ.d.cts → types-DwNN5cWe.d.cts} +82 -2
- package/dist/{types-BzUEVkxJ.d.ts → types-DwNN5cWe.d.ts} +82 -2
- package/dist/vite.d.cts +1 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vitest-setup.cjs +357 -44
- 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 +357 -44
- package/dist/vitest-setup.js.map +1 -1
- package/dist/vitest.cjs +420 -80
- 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 +421 -81
- package/dist/vitest.js.map +1 -1
- package/package.json +41 -4
- package/panel/panel.css +49 -0
- package/panel/panel.js +871 -353
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
|
}
|
|
@@ -369,16 +403,27 @@ function printReport(report, options) {
|
|
|
369
403
|
}
|
|
370
404
|
|
|
371
405
|
// src/state.ts
|
|
406
|
+
var MAX_SEEN_COMPONENTS = 500;
|
|
372
407
|
var KEY = /* @__PURE__ */ Symbol.for("rerender-lens.state");
|
|
373
408
|
function getState() {
|
|
374
409
|
const g = globalThis;
|
|
375
410
|
let s = g[KEY];
|
|
376
411
|
if (!s) {
|
|
377
|
-
s = { options: {}, enabled: false, printed: /* @__PURE__ */ new Map(), detach: null, nextInstanceId: 1, nextCommitId: 1, warnedOnce: /* @__PURE__ */ new Set(), scheduled: 0, commits: 0, overheadMs: 0, maxCommitMs: 0, truncated: 0 };
|
|
412
|
+
s = { options: {}, enabled: false, printed: /* @__PURE__ */ new Map(), detach: null, nextInstanceId: 1, nextCommitId: 1, warnedOnce: /* @__PURE__ */ new Set(), scheduled: 0, commits: 0, overheadMs: 0, maxCommitMs: 0, truncated: 0, seen: /* @__PURE__ */ new Set(), seenTracked: /* @__PURE__ */ new Set(), seenOverflow: false };
|
|
378
413
|
g[KEY] = s;
|
|
379
414
|
}
|
|
380
415
|
return s;
|
|
381
416
|
}
|
|
417
|
+
function noteRendered(s, name, tracked) {
|
|
418
|
+
if (!s.seen.has(name)) {
|
|
419
|
+
if (s.seen.size >= MAX_SEEN_COMPONENTS) {
|
|
420
|
+
s.seenOverflow = true;
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
s.seen.add(name);
|
|
424
|
+
}
|
|
425
|
+
if (tracked) s.seenTracked.add(name);
|
|
426
|
+
}
|
|
382
427
|
function warnOnce(key, message) {
|
|
383
428
|
const s = getState();
|
|
384
429
|
if (s.warnedOnce.has(key)) return;
|
|
@@ -410,6 +455,27 @@ function dispatch(report, override) {
|
|
|
410
455
|
}
|
|
411
456
|
|
|
412
457
|
// src/hookNames.ts
|
|
458
|
+
var NODE_COUNT = {
|
|
459
|
+
useState: 1,
|
|
460
|
+
useReducer: 1,
|
|
461
|
+
useRef: 1,
|
|
462
|
+
useMemo: 1,
|
|
463
|
+
useCallback: 1,
|
|
464
|
+
useEffect: 1,
|
|
465
|
+
useLayoutEffect: 1,
|
|
466
|
+
useInsertionEffect: 1,
|
|
467
|
+
useImperativeHandle: 1,
|
|
468
|
+
useSyncExternalStore: 2,
|
|
469
|
+
useTransition: 2,
|
|
470
|
+
useDeferredValue: 1,
|
|
471
|
+
useId: 1,
|
|
472
|
+
useOptimistic: 1,
|
|
473
|
+
useActionState: 3,
|
|
474
|
+
useFormState: 3,
|
|
475
|
+
useEffectEvent: 1,
|
|
476
|
+
useCacheRefresh: 1
|
|
477
|
+
};
|
|
478
|
+
var BUILTIN_HOOKS = /* @__PURE__ */ new Set([...Object.keys(NODE_COUNT), "useContext", "useDebugValue", "use"]);
|
|
413
479
|
var noop = () => {
|
|
414
480
|
};
|
|
415
481
|
function customHooksFromStack(stack, componentName, marker) {
|
|
@@ -424,7 +490,7 @@ function customHooksFromStack(stack, componentName, marker) {
|
|
|
424
490
|
continue;
|
|
425
491
|
}
|
|
426
492
|
if (name === componentName || name.endsWith("." + componentName)) break;
|
|
427
|
-
if (/^use[A-Z0-9_]/.test(name)) names.push(name);
|
|
493
|
+
if (/^use[A-Z0-9_]/.test(name) && !BUILTIN_HOOKS.has(name)) names.push(name);
|
|
428
494
|
if (names.length > 8) break;
|
|
429
495
|
}
|
|
430
496
|
return names;
|
|
@@ -724,6 +790,11 @@ function isMemoizedFiber(fiber) {
|
|
|
724
790
|
}
|
|
725
791
|
return false;
|
|
726
792
|
}
|
|
793
|
+
function isCompiledFiber(fiber) {
|
|
794
|
+
if (fiber.tag === ClassComponent) return false;
|
|
795
|
+
const cache2 = fiber.updateQueue?.memoCache;
|
|
796
|
+
return !!cache2 && Array.isArray(cache2.data);
|
|
797
|
+
}
|
|
727
798
|
function durationsOf(fiber) {
|
|
728
799
|
if (typeof fiber.actualDuration !== "number") return null;
|
|
729
800
|
const tree = fiber.actualDuration;
|
|
@@ -902,7 +973,8 @@ function diffHooks(fiber, alt) {
|
|
|
902
973
|
const labels = hookLabelsFor(fiber, b);
|
|
903
974
|
let i = 0;
|
|
904
975
|
while (a && b) {
|
|
905
|
-
|
|
976
|
+
const deferred = labels?.[i] === "useDeferredValue";
|
|
977
|
+
if ((deferred || isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
|
|
906
978
|
const hook = labels?.[i] ?? hookLabel2(b);
|
|
907
979
|
out.push({
|
|
908
980
|
path: `${hook}#${i}`,
|
|
@@ -936,6 +1008,10 @@ function diffContexts(fiber, alt) {
|
|
|
936
1008
|
let a = alt.dependencies?.firstContext ?? null;
|
|
937
1009
|
let b = fiber.dependencies?.firstContext ?? null;
|
|
938
1010
|
let i = 0;
|
|
1011
|
+
if (b && !("memoizedValue" in b)) {
|
|
1012
|
+
warnOnce("contextValues", "this React does not record context values on the fiber (React < 18): context changes are reported as plain parent re-renders.");
|
|
1013
|
+
return out;
|
|
1014
|
+
}
|
|
939
1015
|
while (a && b) {
|
|
940
1016
|
if (a.context === b.context && !Object.is(a.memoizedValue, b.memoizedValue)) {
|
|
941
1017
|
const name = b.context.displayName ?? "Context";
|
|
@@ -968,10 +1044,26 @@ function diffContexts(fiber, alt) {
|
|
|
968
1044
|
return out;
|
|
969
1045
|
}
|
|
970
1046
|
var isGenuine2 = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
|
|
1047
|
+
var isRefObject2 = (v) => {
|
|
1048
|
+
if (typeof v !== "object" || v === null) return false;
|
|
1049
|
+
const keys = Object.keys(v);
|
|
1050
|
+
return keys.length === 1 && keys[0] === "current";
|
|
1051
|
+
};
|
|
1052
|
+
function diffProps(prev, next) {
|
|
1053
|
+
if (prev === next) return [];
|
|
1054
|
+
const pr = prev.ref;
|
|
1055
|
+
const nr = next.ref;
|
|
1056
|
+
if (pr !== nr && isRefObject2(pr) && isRefObject2(nr)) {
|
|
1057
|
+
const { ref: _p, ...restPrev } = prev;
|
|
1058
|
+
const { ref: _n, ...restNext } = next;
|
|
1059
|
+
return [{ path: "ref", kind: "deep-equal", prev: pr, next: nr }, ...diffRecords(restPrev, restNext)];
|
|
1060
|
+
}
|
|
1061
|
+
return diffRecords(prev, next);
|
|
1062
|
+
}
|
|
971
1063
|
function analyze(fiber, alt, trackHooks) {
|
|
972
1064
|
const prevProps = alt.memoizedProps ?? {};
|
|
973
1065
|
const nextProps = fiber.memoizedProps ?? {};
|
|
974
|
-
const propChanges =
|
|
1066
|
+
const propChanges = diffProps(prevProps, nextProps);
|
|
975
1067
|
let stateChanges = [];
|
|
976
1068
|
let hookChanges = [];
|
|
977
1069
|
if (fiber.tag === ClassComponent) {
|
|
@@ -984,7 +1076,7 @@ function analyze(fiber, alt, trackHooks) {
|
|
|
984
1076
|
} else if (trackHooks) {
|
|
985
1077
|
hookChanges = [...diffHooks(fiber, alt), ...diffContexts(fiber, alt)];
|
|
986
1078
|
}
|
|
987
|
-
const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore";
|
|
1079
|
+
const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore" && h.hook !== "useDeferredValue";
|
|
988
1080
|
const causes = [];
|
|
989
1081
|
if (propChanges.some(isGenuine2)) causes.push("props");
|
|
990
1082
|
if (stateChanges.some(isGenuine2) || hookChanges.some((h) => isStateHook(h) && isGenuine2(h))) causes.push("state");
|
|
@@ -1024,6 +1116,24 @@ var lastCommit = null;
|
|
|
1024
1116
|
var EFFECT_LOOP_WINDOW_MS = 50;
|
|
1025
1117
|
var MAX_REPORTS_PER_COMMIT = 200;
|
|
1026
1118
|
var COMMIT_TIME_BUDGET_MS = 25;
|
|
1119
|
+
function isDeferredCatchUp(fiber) {
|
|
1120
|
+
const types = fiber._debugHookTypes;
|
|
1121
|
+
if (!types || !types.includes("useDeferredValue") || !fiber.alternate) return false;
|
|
1122
|
+
const first = fiber.memoizedState;
|
|
1123
|
+
if (!isHookList(first)) return false;
|
|
1124
|
+
const labels = hookLabelsFor(fiber, first);
|
|
1125
|
+
if (!labels) return false;
|
|
1126
|
+
let a = fiber.alternate.memoizedState;
|
|
1127
|
+
let b = first;
|
|
1128
|
+
for (let i = 0; a && b; a = a.next, b = b.next, i++) {
|
|
1129
|
+
if (labels[i] === "useDeferredValue" && !Object.is(a.memoizedState, b.memoizedState)) return true;
|
|
1130
|
+
}
|
|
1131
|
+
return false;
|
|
1132
|
+
}
|
|
1133
|
+
function isUnder(fiber, boundaries) {
|
|
1134
|
+
for (let f = fiber.return; f && f.tag !== HostRoot; f = f.return) if (f.tag === SuspenseComponent && boundaries.includes(f)) return true;
|
|
1135
|
+
return false;
|
|
1136
|
+
}
|
|
1027
1137
|
function onCommit(root, commitPriority) {
|
|
1028
1138
|
const s = getState();
|
|
1029
1139
|
if (!s.enabled) return;
|
|
@@ -1033,17 +1143,20 @@ function onCommit(root, commitPriority) {
|
|
|
1033
1143
|
const rendered = [];
|
|
1034
1144
|
const renderedNames = /* @__PURE__ */ new Set();
|
|
1035
1145
|
let hot = false;
|
|
1036
|
-
let
|
|
1146
|
+
let resolvedBoundaries = null;
|
|
1037
1147
|
const stack = [root.current];
|
|
1038
1148
|
while (stack.length) {
|
|
1039
1149
|
const fiber = stack.pop();
|
|
1040
1150
|
const alt = fiber.alternate;
|
|
1041
1151
|
if (alt && isComponentTag(fiber.tag) && didRender(fiber)) {
|
|
1042
1152
|
if (isHotSwapped(fiber, alt)) hot = true;
|
|
1043
|
-
|
|
1044
|
-
|
|
1153
|
+
const name = fiberName(fiber);
|
|
1154
|
+
renderedNames.add(name);
|
|
1155
|
+
const tracked = shouldTrack(fiberType(fiber), o);
|
|
1156
|
+
noteRendered(s, name, tracked);
|
|
1157
|
+
if (tracked) rendered.push(fiber);
|
|
1045
1158
|
}
|
|
1046
|
-
if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null)
|
|
1159
|
+
if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) (resolvedBoundaries ?? (resolvedBoundaries = [])).push(fiber);
|
|
1047
1160
|
if (!alt || fiber.child !== alt.child) {
|
|
1048
1161
|
for (let c = fiber.child; c; c = c.sibling) stack.push(c);
|
|
1049
1162
|
}
|
|
@@ -1055,10 +1168,10 @@ function onCommit(root, commitPriority) {
|
|
|
1055
1168
|
let commitCause;
|
|
1056
1169
|
let afterCommit;
|
|
1057
1170
|
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))) {
|
|
1171
|
+
if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u)) && !rendered.some(isDeferredCatchUp)) {
|
|
1059
1172
|
commitCause = "effect-after-commit";
|
|
1060
1173
|
afterCommit = lastCommit.id;
|
|
1061
|
-
} else if (
|
|
1174
|
+
} else if (resolvedBoundaries) commitCause = "suspense-resolved";
|
|
1062
1175
|
if (rendered.length === 0) {
|
|
1063
1176
|
lastCommit = { id: s.nextCommitId, at, rendered: renderedNames };
|
|
1064
1177
|
return;
|
|
@@ -1110,6 +1223,9 @@ function onCommit(root, commitPriority) {
|
|
|
1110
1223
|
owner: ownerName(fiber),
|
|
1111
1224
|
path: componentPath(fiber),
|
|
1112
1225
|
memoized: isMemoizedFiber(fiber),
|
|
1226
|
+
compiled: isCompiledFiber(fiber),
|
|
1227
|
+
classComponent: fiber.tag === ClassComponent,
|
|
1228
|
+
revealed: resolvedBoundaries !== null && a.trigger === "parent" && isUnder(fiber, resolvedBoundaries),
|
|
1113
1229
|
selfDuration: durations ? durations.self : void 0,
|
|
1114
1230
|
treeDuration: durations ? durations.tree : void 0,
|
|
1115
1231
|
commitId,
|
|
@@ -1163,10 +1279,53 @@ function shouldTrack(type, o) {
|
|
|
1163
1279
|
if (o.trackAllMemoized) return isMemo(type) || isPureClass(type);
|
|
1164
1280
|
return false;
|
|
1165
1281
|
}
|
|
1282
|
+
var ANONYMOUS = "Anonymous";
|
|
1283
|
+
function explainTracking(type, o) {
|
|
1284
|
+
if (typeof type === "string")
|
|
1285
|
+
return { tracked: false, name: type, memoized: false, reason: `<${type}> is a DOM element, not a React component; only components are tracked.` };
|
|
1286
|
+
if (!isComponentLike(type))
|
|
1287
|
+
return { tracked: false, name: ANONYMOUS, memoized: false, reason: "This is not a React component type, so it is never tracked." };
|
|
1288
|
+
const name = getDisplayName(type);
|
|
1289
|
+
const memo = isMemo(type);
|
|
1290
|
+
const memoized = memo || isPureClass(type);
|
|
1291
|
+
if (o.exclude?.some((m) => matches(m, name)))
|
|
1292
|
+
return { tracked: false, name, memoized, reason: `<${name}> matches "exclude", which wins over every other rule.`, fix: `Remove "${name}" from the exclude list.` };
|
|
1293
|
+
if (hasMarker(type)) return { tracked: true, name, memoized, reason: `<${name}> is marked with track() (the ${MARKER} static).` };
|
|
1294
|
+
if (o.include?.some((m) => matches(m, name))) return { tracked: true, name, memoized, reason: `<${name}> matches "include".` };
|
|
1295
|
+
if (o.trackAllComponents) return { tracked: true, name, memoized, reason: '"Track every component" is on, so every component is tracked.' };
|
|
1296
|
+
if (o.trackAllMemoized && memoized)
|
|
1297
|
+
return { tracked: true, name, memoized, reason: `<${name}> is ${memo ? "a React.memo component" : "a PureComponent"}, and "Track every React.memo and PureComponent" is on.` };
|
|
1298
|
+
if (name === ANONYMOUS)
|
|
1299
|
+
return {
|
|
1300
|
+
tracked: false,
|
|
1301
|
+
name,
|
|
1302
|
+
memoized,
|
|
1303
|
+
reason: 'This component has no display name, so "include" and "exclude" cannot address it.',
|
|
1304
|
+
fix: "Give the function a name, set displayName, or mark it with track(fn, 'Name')."
|
|
1305
|
+
};
|
|
1306
|
+
if (o.trackAllMemoized)
|
|
1307
|
+
return {
|
|
1308
|
+
tracked: false,
|
|
1309
|
+
name,
|
|
1310
|
+
memoized,
|
|
1311
|
+
reason: `"Track every React.memo and PureComponent" only covers React.memo components and PureComponent classes, and <${name}> is neither.`,
|
|
1312
|
+
fix: `Add "${name}" to include, wrap it in React.memo, or turn on "Track every component".`
|
|
1313
|
+
};
|
|
1314
|
+
return {
|
|
1315
|
+
tracked: false,
|
|
1316
|
+
name,
|
|
1317
|
+
memoized,
|
|
1318
|
+
reason: `Nothing selects <${name}>: it is not marked with track(), no "include" pattern matches it, and neither "track every" option is on.`,
|
|
1319
|
+
fix: `Add "${name}" to include, or turn on "Track every React.memo and PureComponent" / "Track every component".`
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1166
1322
|
function init(options = {}) {
|
|
1167
1323
|
const s = getState();
|
|
1168
1324
|
s.options = { ...options };
|
|
1169
1325
|
s.printed.clear();
|
|
1326
|
+
s.seen.clear();
|
|
1327
|
+
s.seenTracked.clear();
|
|
1328
|
+
s.seenOverflow = false;
|
|
1170
1329
|
if (!s.enabled) {
|
|
1171
1330
|
s.detach = attach();
|
|
1172
1331
|
s.enabled = true;
|
|
@@ -1221,38 +1380,270 @@ function useWhyRerender(name, values, options) {
|
|
|
1221
1380
|
});
|
|
1222
1381
|
}
|
|
1223
1382
|
|
|
1383
|
+
// src/causes.ts
|
|
1384
|
+
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);
|
|
1385
|
+
var bump = (m, key, n = 1) => void m.set(key, (m.get(key) || 0) + n);
|
|
1386
|
+
function indexByComponent(reports) {
|
|
1387
|
+
const index = /* @__PURE__ */ new Map();
|
|
1388
|
+
for (const r of reports) {
|
|
1389
|
+
const list = index.get(r.component);
|
|
1390
|
+
if (list) list.push(r);
|
|
1391
|
+
else index.set(r.component, [r]);
|
|
1392
|
+
}
|
|
1393
|
+
return index;
|
|
1394
|
+
}
|
|
1395
|
+
function rootCauseOf(r, commitReports, index = indexByComponent(commitReports)) {
|
|
1396
|
+
let cur = r;
|
|
1397
|
+
const seen = /* @__PURE__ */ new Set([r]);
|
|
1398
|
+
while (cur.trigger === "parent" && cur.parent) {
|
|
1399
|
+
const parent = cur.parent;
|
|
1400
|
+
const candidates = index.get(parent.name);
|
|
1401
|
+
const p = candidates && candidates.find((x) => isAncestorReport(x, cur));
|
|
1402
|
+
if (!p || seen.has(p)) return { name: parent.name, trigger: parent.trigger, report: null };
|
|
1403
|
+
seen.add(p);
|
|
1404
|
+
cur = p;
|
|
1405
|
+
}
|
|
1406
|
+
return cur === r ? null : { name: cur.component, trigger: cur.trigger, report: cur };
|
|
1407
|
+
}
|
|
1408
|
+
function rootCausesOf(reports) {
|
|
1409
|
+
const roots = /* @__PURE__ */ new Map();
|
|
1410
|
+
const rootByReport = /* @__PURE__ */ new Map();
|
|
1411
|
+
const index = indexByComponent(reports);
|
|
1412
|
+
let avoidable = 0;
|
|
1413
|
+
let wasted = 0;
|
|
1414
|
+
for (const r of reports) {
|
|
1415
|
+
if (!r.avoidable) continue;
|
|
1416
|
+
avoidable++;
|
|
1417
|
+
if (typeof r.selfDuration === "number") wasted += r.selfDuration;
|
|
1418
|
+
const root = rootCauseOf(r, reports, index);
|
|
1419
|
+
const name = root ? root.name : r.parent && r.parent.name || "(unknown)";
|
|
1420
|
+
const trigger = root ? root.trigger : r.parent && r.parent.trigger || "parent";
|
|
1421
|
+
rootByReport.set(r, name);
|
|
1422
|
+
let agg = roots.get(name);
|
|
1423
|
+
if (!agg) {
|
|
1424
|
+
agg = { name, trigger, count: 0, components: /* @__PURE__ */ new Map() };
|
|
1425
|
+
roots.set(name, agg);
|
|
1426
|
+
}
|
|
1427
|
+
agg.count++;
|
|
1428
|
+
bump(agg.components, r.component);
|
|
1429
|
+
}
|
|
1430
|
+
return { roots: [...roots.values()].sort((a, b) => b.count - a.count), rootByReport, avoidable, wasted };
|
|
1431
|
+
}
|
|
1432
|
+
function analyzeCommit(reports) {
|
|
1433
|
+
const first = reports[0];
|
|
1434
|
+
return {
|
|
1435
|
+
id: first && typeof first.commitId === "number" ? first.commitId : 0,
|
|
1436
|
+
total: reports.length,
|
|
1437
|
+
...rootCausesOf(reports),
|
|
1438
|
+
contexts: contextAttribution(reports),
|
|
1439
|
+
reports
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
function contextAttribution(reports) {
|
|
1443
|
+
const byCtx = /* @__PURE__ */ new Map();
|
|
1444
|
+
for (const r of reports) {
|
|
1445
|
+
for (const c of r.hookChanges || []) {
|
|
1446
|
+
if (c.hook !== "useContext" && !/^useContext/.test(c.path)) continue;
|
|
1447
|
+
const m = /useContext\((.*)\)/.exec(c.path);
|
|
1448
|
+
const name = m && m[1] ? m[1] : c.path;
|
|
1449
|
+
let agg = byCtx.get(name);
|
|
1450
|
+
if (!agg) {
|
|
1451
|
+
agg = { name, consumers: 0, avoidable: 0, components: /* @__PURE__ */ new Map(), commits: /* @__PURE__ */ new Set(), providers: /* @__PURE__ */ new Map(), changedKeys: /* @__PURE__ */ new Set(), totalKeys: 0 };
|
|
1452
|
+
byCtx.set(name, agg);
|
|
1453
|
+
}
|
|
1454
|
+
agg.consumers++;
|
|
1455
|
+
if (AVOIDABLE_KINDS.has(c.kind)) agg.avoidable++;
|
|
1456
|
+
bump(agg.components, r.component);
|
|
1457
|
+
agg.commits.add(typeof r.commitId === "number" ? r.commitId : 0);
|
|
1458
|
+
if (c.provider && c.provider.component) bump(agg.providers, c.provider.component);
|
|
1459
|
+
if (c.changedKeys) for (const k of c.changedKeys) agg.changedKeys.add(k);
|
|
1460
|
+
if (typeof c.totalKeys === "number") agg.totalKeys = Math.max(agg.totalKeys, c.totalKeys);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
return [...byCtx.values()].sort((a, b) => b.consumers - a.consumers);
|
|
1464
|
+
}
|
|
1465
|
+
function cascadeTree(reports) {
|
|
1466
|
+
const root = { name: "", children: /* @__PURE__ */ new Map(), report: null };
|
|
1467
|
+
for (const r of reports) {
|
|
1468
|
+
let node = root;
|
|
1469
|
+
for (const seg of r.path.concat([r.component])) {
|
|
1470
|
+
let next = node.children.get(seg);
|
|
1471
|
+
if (!next) {
|
|
1472
|
+
next = { name: seg, children: /* @__PURE__ */ new Map(), report: null };
|
|
1473
|
+
node.children.set(seg, next);
|
|
1474
|
+
}
|
|
1475
|
+
node = next;
|
|
1476
|
+
}
|
|
1477
|
+
if (!node.report || r.avoidable) node.report = r;
|
|
1478
|
+
node.count = (node.count || 0) + 1;
|
|
1479
|
+
if (r.avoidable) node.avoidable = (node.avoidable || 0) + 1;
|
|
1480
|
+
}
|
|
1481
|
+
return root;
|
|
1482
|
+
}
|
|
1483
|
+
function rootCauseSummary(name, commits, analyze2) {
|
|
1484
|
+
const analyzeFn = analyze2 || ((_, reports) => analyzeCommit(reports));
|
|
1485
|
+
const out = { name, trigger: "parent", commits: [], total: 0, components: /* @__PURE__ */ new Map(), affected: [] };
|
|
1486
|
+
for (const [key, reports] of commits) {
|
|
1487
|
+
const analysis = analyzeFn(key, reports);
|
|
1488
|
+
const root = analysis.roots.find((x) => x.name === name);
|
|
1489
|
+
if (!root) continue;
|
|
1490
|
+
out.trigger = root.trigger;
|
|
1491
|
+
out.commits.push({ key, analysis, count: root.count, components: root.components });
|
|
1492
|
+
out.total += root.count;
|
|
1493
|
+
for (const [c, n] of root.components) bump(out.components, c, n);
|
|
1494
|
+
for (const r of reports) if (r.avoidable && analysis.rootByReport.get(r) === name) out.affected.push(r);
|
|
1495
|
+
}
|
|
1496
|
+
out.commits.reverse();
|
|
1497
|
+
return out;
|
|
1498
|
+
}
|
|
1499
|
+
function groupByCommit(reports) {
|
|
1500
|
+
const out = /* @__PURE__ */ new Map();
|
|
1501
|
+
for (const r of reports) {
|
|
1502
|
+
if (typeof r.commitId !== "number" || r.commitId <= 0) continue;
|
|
1503
|
+
const list = out.get(r.commitId);
|
|
1504
|
+
if (list) list.push(r);
|
|
1505
|
+
else out.set(r.commitId, [r]);
|
|
1506
|
+
}
|
|
1507
|
+
return out;
|
|
1508
|
+
}
|
|
1509
|
+
function rankRootCauses(reports) {
|
|
1510
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1511
|
+
for (const commit of groupByCommit(reports).values()) {
|
|
1512
|
+
for (const root of rootCausesOf(commit).roots) {
|
|
1513
|
+
let agg = byName.get(root.name);
|
|
1514
|
+
if (!agg) {
|
|
1515
|
+
agg = { name: root.name, trigger: root.trigger, commits: 0, count: 0, components: /* @__PURE__ */ new Map() };
|
|
1516
|
+
byName.set(root.name, agg);
|
|
1517
|
+
}
|
|
1518
|
+
agg.commits++;
|
|
1519
|
+
agg.count += root.count;
|
|
1520
|
+
for (const [c, n] of root.components) bump(agg.components, c, n);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
return [...byName.values()].sort((a, b) => b.count - a.count || b.commits - a.commits || a.name.localeCompare(b.name));
|
|
1524
|
+
}
|
|
1525
|
+
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
1526
|
+
function formatRootCauses(reports, limit = 5) {
|
|
1527
|
+
const ranked = rankRootCauses(reports);
|
|
1528
|
+
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(", ")})`);
|
|
1529
|
+
if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
|
|
1530
|
+
return lines.join("\n");
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1224
1533
|
// src/fixes.ts
|
|
1225
|
-
var
|
|
1534
|
+
var MAX_FIX_REPORTS = 50;
|
|
1226
1535
|
var rootOf = (path) => path.split(/[.[]/)[0] || path;
|
|
1536
|
+
var identifier = (name) => /^[A-Za-z_$][\w$]*$/.test(name) ? name : "value";
|
|
1537
|
+
function shortValue(v, max = 60) {
|
|
1538
|
+
let s;
|
|
1539
|
+
try {
|
|
1540
|
+
s = JSON.stringify(v);
|
|
1541
|
+
} catch {
|
|
1542
|
+
s = String(v);
|
|
1543
|
+
}
|
|
1544
|
+
if (s === void 0) s = String(v);
|
|
1545
|
+
return s.length > max ? s.slice(0, max - 3) + "..." : s;
|
|
1546
|
+
}
|
|
1227
1547
|
function fixesFor(r) {
|
|
1228
1548
|
const out = [];
|
|
1229
|
-
const
|
|
1230
|
-
const
|
|
1549
|
+
const ownerName2 = r.owner || r.parent && r.parent.name || null;
|
|
1550
|
+
const propChanges = r.propChanges || [];
|
|
1551
|
+
const changes = [...propChanges, ...r.stateChanges || [], ...r.hookChanges || []];
|
|
1231
1552
|
if (r.avoidable && (changes.length === 0 || r.memoized === false)) {
|
|
1553
|
+
const identical = changes.length === 0;
|
|
1232
1554
|
out.push({
|
|
1233
1555
|
kind: "memo",
|
|
1234
1556
|
owner: r.component,
|
|
1235
1557
|
target: r.component,
|
|
1236
1558
|
prop: null,
|
|
1237
1559
|
label: `Wrap <${r.component}> in React.memo`,
|
|
1238
|
-
detail:
|
|
1560
|
+
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.`,
|
|
1561
|
+
snippet: `// ${r.component}
|
|
1562
|
+
import { memo } from 'react';
|
|
1563
|
+
|
|
1564
|
+
export const ${r.component} = memo(function ${r.component}(props) {
|
|
1565
|
+
// ...
|
|
1566
|
+
});
|
|
1567
|
+
// class components: extend PureComponent instead`
|
|
1239
1568
|
});
|
|
1240
1569
|
}
|
|
1241
|
-
for (const c of
|
|
1242
|
-
if (!
|
|
1570
|
+
for (const c of propChanges) {
|
|
1571
|
+
if (!AVOIDABLE_KINDS.has(c.kind)) continue;
|
|
1572
|
+
const owner = ownerName2 || "?";
|
|
1243
1573
|
const root = rootOf(c.path);
|
|
1574
|
+
const id = identifier(root);
|
|
1244
1575
|
if (root === "children" && (c.kind === "element" || c.kind === "deep-equal")) {
|
|
1245
|
-
out.push({
|
|
1576
|
+
out.push({
|
|
1577
|
+
kind: "children",
|
|
1578
|
+
owner,
|
|
1579
|
+
target: r.component,
|
|
1580
|
+
prop: "children",
|
|
1581
|
+
label: `memoize children of <${r.component}> in <${owner}>`,
|
|
1582
|
+
detail: `<${owner}> re-creates the children of <${r.component}> on every render; they have the same types and props each time.`,
|
|
1583
|
+
snippet: `// ${owner}
|
|
1584
|
+
import { useMemo } from 'react';
|
|
1585
|
+
|
|
1586
|
+
const children = useMemo(() => (
|
|
1587
|
+
<>{/* the same elements */}</>
|
|
1588
|
+
), [/* deps */]);
|
|
1589
|
+
|
|
1590
|
+
<${r.component}>{children}</${r.component}>
|
|
1591
|
+
|
|
1592
|
+
// static children: hoist them to module scope
|
|
1593
|
+
const STATIC = <em>hi</em>;`
|
|
1594
|
+
});
|
|
1246
1595
|
} else if (c.kind === "function") {
|
|
1247
|
-
out.push({
|
|
1596
|
+
out.push({
|
|
1597
|
+
kind: "useCallback",
|
|
1598
|
+
owner,
|
|
1599
|
+
target: r.component,
|
|
1600
|
+
prop: root,
|
|
1601
|
+
label: `useCallback(${root}) in <${owner}>`,
|
|
1602
|
+
detail: `prop "${c.path}" of <${r.component}> is a new function on every render of <${owner}>.`,
|
|
1603
|
+
snippet: `// ${owner}
|
|
1604
|
+
import { useCallback } from 'react';
|
|
1605
|
+
|
|
1606
|
+
const ${id} = useCallback((/* args */) => {
|
|
1607
|
+
// ...
|
|
1608
|
+
}, [/* deps */]);
|
|
1609
|
+
|
|
1610
|
+
<${r.component} ${root}={${id}} />`
|
|
1611
|
+
});
|
|
1248
1612
|
} else if (c.kind === "element") {
|
|
1249
|
-
out.push({
|
|
1613
|
+
out.push({
|
|
1614
|
+
kind: "useMemoElement",
|
|
1615
|
+
owner,
|
|
1616
|
+
target: r.component,
|
|
1617
|
+
prop: root,
|
|
1618
|
+
label: `memoize element prop ${root} in <${owner}>`,
|
|
1619
|
+
detail: `prop "${c.path}" of <${r.component}> is a new element with the same type and props on every render of <${owner}>.`,
|
|
1620
|
+
snippet: `// ${owner}
|
|
1621
|
+
import { useMemo } from 'react';
|
|
1622
|
+
|
|
1623
|
+
const ${id} = useMemo(() => ${shortValue(c.next, 40)}, [/* deps */]);
|
|
1624
|
+
// or pass it as children from a component that does not re-render`
|
|
1625
|
+
});
|
|
1250
1626
|
} else {
|
|
1251
|
-
|
|
1627
|
+
const isArray = Array.isArray(c.next);
|
|
1628
|
+
out.push({
|
|
1629
|
+
kind: "useMemo",
|
|
1630
|
+
owner,
|
|
1631
|
+
target: r.component,
|
|
1632
|
+
prop: root,
|
|
1633
|
+
label: `useMemo(${root}) in <${owner}>`,
|
|
1634
|
+
detail: `prop "${c.path}" of <${r.component}> is a new ${isArray ? "array" : "object"} with the same contents on every render of <${owner}>.`,
|
|
1635
|
+
snippet: `// ${owner}
|
|
1636
|
+
import { useMemo } from 'react';
|
|
1637
|
+
|
|
1638
|
+
const ${id} = useMemo(() => (${shortValue(c.next, 80)}), [/* deps */]);
|
|
1639
|
+
|
|
1640
|
+
// or, when it never changes, hoist it to module scope:
|
|
1641
|
+
const ${id.toUpperCase()} = ${shortValue(c.next, 80)};`
|
|
1642
|
+
});
|
|
1252
1643
|
}
|
|
1253
1644
|
}
|
|
1254
|
-
for (const c of [...r.stateChanges, ...r.hookChanges]) {
|
|
1255
|
-
const isContext = c.hook === "useContext";
|
|
1645
|
+
for (const c of [...r.stateChanges || [], ...r.hookChanges || []]) {
|
|
1646
|
+
const isContext = c.hook === "useContext" || /^useContext/.test(c.path);
|
|
1256
1647
|
const ctxName = isContext ? (/useContext\((.*)\)/.exec(c.path) || [])[1] || "Context" : "";
|
|
1257
1648
|
const providerOwner = isContext && c.provider && c.provider.component ? c.provider.component : null;
|
|
1258
1649
|
if (isContext && c.kind === "different" && c.changedKeys && typeof c.totalKeys === "number" && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys) {
|
|
@@ -1262,20 +1653,63 @@ function fixesFor(r) {
|
|
|
1262
1653
|
target: r.component,
|
|
1263
1654
|
prop: ctxName,
|
|
1264
1655
|
label: `split ${ctxName}${providerOwner ? ` in <${providerOwner}>` : ""}: only ${c.changedKeys.join(", ")} changed`,
|
|
1265
|
-
detail: `${c.changedKeys.
|
|
1656
|
+
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.`,
|
|
1657
|
+
snippet: `// ${providerOwner || "Provider"}
|
|
1658
|
+
// one context per independently-changing slice
|
|
1659
|
+
const ${identifier(ctxName)}Static = createContext(...);
|
|
1660
|
+
const ${identifier(ctxName)}${c.changedKeys.map((k) => k[0].toUpperCase() + k.slice(1)).join("")} = createContext(...);
|
|
1661
|
+
|
|
1662
|
+
// or keep one context and let consumers select a slice:
|
|
1663
|
+
const ${c.changedKeys[0]} = useContextSelector(${ctxName}, (v) => v.${c.changedKeys[0]});`
|
|
1266
1664
|
});
|
|
1267
1665
|
continue;
|
|
1268
1666
|
}
|
|
1269
|
-
if (!
|
|
1667
|
+
if (!AVOIDABLE_KINDS.has(c.kind)) continue;
|
|
1270
1668
|
if (isContext) {
|
|
1271
|
-
out.push({
|
|
1669
|
+
out.push({
|
|
1670
|
+
kind: "contextValue",
|
|
1671
|
+
owner: providerOwner || `${ctxName}.Provider`,
|
|
1672
|
+
target: r.component,
|
|
1673
|
+
prop: ctxName,
|
|
1674
|
+
label: `memoize the ${ctxName} provider value${providerOwner ? ` in <${providerOwner}>` : ""}`,
|
|
1675
|
+
detail: `<${r.component}> re-rendered because ${ctxName} produced a new value that is deep-equal to the previous one${providerOwner ? ` (Provider rendered by <${providerOwner}>)` : ""}.`,
|
|
1676
|
+
snippet: `// ${providerOwner || `where <${ctxName}.Provider> is rendered`}
|
|
1677
|
+
const value = useMemo(() => ({ /* ... */ }), [/* deps */]);
|
|
1678
|
+
<${ctxName}.Provider value={value}>`
|
|
1679
|
+
});
|
|
1272
1680
|
} else if (c.hook === "useSyncExternalStore") {
|
|
1273
1681
|
const chain = c.custom || [];
|
|
1274
1682
|
const redux = chain.some((n) => /^use(App)?Selector$/.test(n));
|
|
1275
1683
|
const zustand = !redux && chain.some((n) => /^use[A-Z]\w*Store$/.test(n) || n === "useStore" || n === "useBoundStore");
|
|
1276
|
-
|
|
1684
|
+
const via = chain.length ? ` via ${chain.join(" \u203A ")}` : "";
|
|
1685
|
+
out.push({
|
|
1686
|
+
kind: "storeSnapshot",
|
|
1687
|
+
owner: r.component,
|
|
1688
|
+
target: r.component,
|
|
1689
|
+
prop: c.path,
|
|
1690
|
+
label: redux ? `memoize the selector in <${r.component}>` : zustand ? `useShallow in <${r.component}>` : `stable getSnapshot in <${r.component}>`,
|
|
1691
|
+
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.`,
|
|
1692
|
+
snippet: redux ? `// ${r.component}
|
|
1693
|
+
import { shallowEqual } from 'react-redux';
|
|
1694
|
+
const slice = useSelector(selectSlice, shallowEqual);
|
|
1695
|
+
// or memoize: const selectSlice = createSelector([selectA, selectB], (a, b) => ({ a, b }));` : zustand ? `// ${r.component}
|
|
1696
|
+
import { useShallow } from 'zustand/react/shallow';
|
|
1697
|
+
const { a, b } = useStore(useShallow((s) => ({ a: s.a, b: s.b })));
|
|
1698
|
+
// or select a primitive: const a = useStore((s) => s.a);` : `// ${r.component}
|
|
1699
|
+
// getSnapshot must return the same reference while the data is unchanged
|
|
1700
|
+
const snapshot = useSyncExternalStore(subscribe, store.getSnapshot /* cached */);`
|
|
1701
|
+
});
|
|
1277
1702
|
} else {
|
|
1278
|
-
out.push({
|
|
1703
|
+
out.push({
|
|
1704
|
+
kind: "bailout",
|
|
1705
|
+
owner: r.component,
|
|
1706
|
+
target: r.component,
|
|
1707
|
+
prop: c.path,
|
|
1708
|
+
label: `bail out before setting ${c.path} in <${r.component}>`,
|
|
1709
|
+
detail: `${c.path} was set to a value deep-equal to the current one (new reference).`,
|
|
1710
|
+
snippet: `// ${r.component}
|
|
1711
|
+
setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
1712
|
+
});
|
|
1279
1713
|
}
|
|
1280
1714
|
}
|
|
1281
1715
|
return out;
|
|
@@ -1289,11 +1723,12 @@ function rankFixes(reports) {
|
|
|
1289
1723
|
const key = fixKey(f);
|
|
1290
1724
|
let agg = byKey.get(key);
|
|
1291
1725
|
if (!agg) {
|
|
1292
|
-
agg = { ...f, key, count: 0, components: {} };
|
|
1726
|
+
agg = { ...f, key, count: 0, components: {}, reports: [] };
|
|
1293
1727
|
byKey.set(key, agg);
|
|
1294
1728
|
}
|
|
1295
1729
|
agg.count++;
|
|
1296
1730
|
agg.components[r.component] = (agg.components[r.component] || 0) + 1;
|
|
1731
|
+
if (agg.reports.length < MAX_FIX_REPORTS) agg.reports.push(r);
|
|
1297
1732
|
}
|
|
1298
1733
|
}
|
|
1299
1734
|
return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
|
|
@@ -1303,6 +1738,8 @@ function formatFixes(reports, limit = 10) {
|
|
|
1303
1738
|
if (!ranked.length) return "No avoidable re-renders.";
|
|
1304
1739
|
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
1740
|
if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
|
|
1741
|
+
const roots = formatRootCauses(reports, 5);
|
|
1742
|
+
if (roots) lines.push("", "Root causes:", roots);
|
|
1306
1743
|
return lines.join("\n");
|
|
1307
1744
|
}
|
|
1308
1745
|
|
|
@@ -1359,6 +1796,8 @@ function compareSummaries(before, after) {
|
|
|
1359
1796
|
regressions: rows.filter((r) => r.delta > 0)
|
|
1360
1797
|
};
|
|
1361
1798
|
}
|
|
1799
|
+
var summarizeSession = (session, reports) => summarizeReports(reports, session);
|
|
1800
|
+
var compareSessions = compareSummaries;
|
|
1362
1801
|
function formatComparison(c) {
|
|
1363
1802
|
const w = Math.max(14, ...c.rows.map((r) => r.component.length + 2));
|
|
1364
1803
|
const line = (label, b, a, d) => `${label.padEnd(w)} ${String(b).padStart(8)} ${String(a).padStart(8)} ${d.padStart(8)}`;
|
|
@@ -1557,7 +1996,7 @@ function flash(target, duration = 500) {
|
|
|
1557
1996
|
}
|
|
1558
1997
|
|
|
1559
1998
|
// src/version.ts
|
|
1560
|
-
var VERSION = "0.
|
|
1999
|
+
var VERSION = "0.7.0" ;
|
|
1561
2000
|
|
|
1562
2001
|
// src/devtools.ts
|
|
1563
2002
|
var DEVTOOLS_MARKER = "__rerenderLens";
|
|
@@ -1683,6 +2122,18 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1683
2122
|
target.postMessage(msg, "*");
|
|
1684
2123
|
};
|
|
1685
2124
|
const source = options.source ?? "page";
|
|
2125
|
+
const trackingSummary = () => {
|
|
2126
|
+
const s = getState();
|
|
2127
|
+
const o = s.options;
|
|
2128
|
+
return {
|
|
2129
|
+
mode: o.trackAllComponents ? "all" : o.trackAllMemoized ? "memoized" : "marked",
|
|
2130
|
+
include: (o.include ?? []).map(matcherToString).filter((x) => x !== null),
|
|
2131
|
+
exclude: (o.exclude ?? []).map(matcherToString).filter((x) => x !== null),
|
|
2132
|
+
renderedCount: s.seen.size,
|
|
2133
|
+
trackedCount: s.seenTracked.size,
|
|
2134
|
+
overflow: s.seenOverflow
|
|
2135
|
+
};
|
|
2136
|
+
};
|
|
1686
2137
|
const info = () => ({
|
|
1687
2138
|
count: buffer.length,
|
|
1688
2139
|
library: VERSION,
|
|
@@ -1696,7 +2147,8 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1696
2147
|
scheduled: getState().scheduled,
|
|
1697
2148
|
commits: getState().commits,
|
|
1698
2149
|
overhead: { totalMs: getState().overheadMs, maxCommitMs: getState().maxCommitMs },
|
|
1699
|
-
truncated: getState().truncated
|
|
2150
|
+
truncated: getState().truncated,
|
|
2151
|
+
tracking: trackingSummary()
|
|
1700
2152
|
});
|
|
1701
2153
|
const bridge = {
|
|
1702
2154
|
replay: () => {
|
|
@@ -1742,6 +2194,10 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1742
2194
|
flashAvoidable: (on) => {
|
|
1743
2195
|
flashOn = !!on;
|
|
1744
2196
|
},
|
|
2197
|
+
explain: (target2) => {
|
|
2198
|
+
const comp = typeof target2 === "number" ? fiberById(target2) : nearestComponent(fiberForNode(target2));
|
|
2199
|
+
return comp ? explainTracking(fiberType(comp), getState().options) : null;
|
|
2200
|
+
},
|
|
1745
2201
|
inspect: (node) => {
|
|
1746
2202
|
const comp = nearestComponent(fiberForNode(node));
|
|
1747
2203
|
if (!comp) return null;
|
|
@@ -1756,6 +2212,7 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1756
2212
|
component: fiberName(comp),
|
|
1757
2213
|
instanceId: id,
|
|
1758
2214
|
tracked: shouldTrack(fiberType(comp), getState().options),
|
|
2215
|
+
tracking: explainTracking(fiberType(comp), getState().options),
|
|
1759
2216
|
path,
|
|
1760
2217
|
reports: id === null ? [] : buffer.filter((e) => e.instanceId === id).slice(-20).map((e) => e.payload)
|
|
1761
2218
|
};
|
|
@@ -1790,6 +2247,9 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1790
2247
|
bridge.flashAvoidable(!!data.arg);
|
|
1791
2248
|
reply.result = true;
|
|
1792
2249
|
break;
|
|
2250
|
+
case "explain":
|
|
2251
|
+
reply.result = bridge.explain(data.arg);
|
|
2252
|
+
break;
|
|
1793
2253
|
default:
|
|
1794
2254
|
reply.error = `unknown command ${String(data.cmd)}`;
|
|
1795
2255
|
}
|
|
@@ -1798,6 +2258,7 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1798
2258
|
}
|
|
1799
2259
|
return reply;
|
|
1800
2260
|
};
|
|
2261
|
+
const isRelayGreeting = (data) => !!data && typeof data === "object" && data.__rerenderLensRelay === true && typeof data.app === "string";
|
|
1801
2262
|
const isCommand = (data) => !!data && typeof data === "object" && data.__rerenderLensCmd === true && typeof data.id === "string";
|
|
1802
2263
|
const envelope = (type, payload) => ({ [DEVTOOLS_MARKER]: true, version: PROTOCOL_VERSION, type, payload });
|
|
1803
2264
|
const sinks = [];
|
|
@@ -1820,14 +2281,20 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1820
2281
|
const relayUrl = options.relay ?? (typeof window !== "undefined" ? window.__RERENDER_LENS_RELAY__ : void 0);
|
|
1821
2282
|
const ES = options.eventSource ?? (typeof EventSource === "function" ? EventSource : null);
|
|
1822
2283
|
if (relayUrl && ES && typeof fetch === "function") {
|
|
1823
|
-
const
|
|
2284
|
+
const [rawBase, relayQuery = ""] = relayUrl.split("?");
|
|
2285
|
+
const base = (rawBase ?? "").replace(/\/$/, "");
|
|
2286
|
+
const relayToken = new URLSearchParams(relayQuery).get("token");
|
|
2287
|
+
const auth = relayToken ? `token=${encodeURIComponent(relayToken)}` : "";
|
|
1824
2288
|
let queue = [];
|
|
1825
2289
|
let scheduled = false;
|
|
2290
|
+
let appId = null;
|
|
1826
2291
|
const flush = () => {
|
|
1827
2292
|
scheduled = false;
|
|
1828
2293
|
const batch = queue;
|
|
1829
2294
|
queue = [];
|
|
1830
|
-
|
|
2295
|
+
const params = [appId ? `app=${encodeURIComponent(appId)}` : "", auth].filter(Boolean).join("&");
|
|
2296
|
+
const to = `${base}/message${params ? `?${params}` : ""}`;
|
|
2297
|
+
fetch(to, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(batch), keepalive: true }).catch(() => {
|
|
1831
2298
|
});
|
|
1832
2299
|
};
|
|
1833
2300
|
const relaySend = (message) => {
|
|
@@ -1838,7 +2305,8 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1838
2305
|
}
|
|
1839
2306
|
};
|
|
1840
2307
|
try {
|
|
1841
|
-
const
|
|
2308
|
+
const label = typeof location !== "undefined" ? `${location.host}${location.pathname}`.slice(0, 120) : "";
|
|
2309
|
+
const stream = new ES(`${base}/events?role=app${label ? `&label=${encodeURIComponent(label)}` : ""}${auth ? `&${auth}` : ""}`);
|
|
1842
2310
|
stream.onmessage = (event) => {
|
|
1843
2311
|
let parsed;
|
|
1844
2312
|
try {
|
|
@@ -1846,7 +2314,10 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1846
2314
|
} catch {
|
|
1847
2315
|
return;
|
|
1848
2316
|
}
|
|
1849
|
-
for (const m of Array.isArray(parsed) ? parsed : [parsed])
|
|
2317
|
+
for (const m of Array.isArray(parsed) ? parsed : [parsed]) {
|
|
2318
|
+
if (isRelayGreeting(m)) appId = m.app;
|
|
2319
|
+
else if (isCommand(m)) relaySend(runCommand(m));
|
|
2320
|
+
}
|
|
1850
2321
|
};
|
|
1851
2322
|
stream.onerror = () => {
|
|
1852
2323
|
};
|
|
@@ -1889,47 +2360,64 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1889
2360
|
};
|
|
1890
2361
|
}
|
|
1891
2362
|
|
|
2363
|
+
exports.AVOIDABLE_KINDS = AVOIDABLE_KINDS;
|
|
1892
2364
|
exports.DEFAULT_CHANNEL = DEFAULT_CHANNEL;
|
|
1893
2365
|
exports.DEVTOOLS_MARKER = DEVTOOLS_MARKER;
|
|
2366
|
+
exports.KIND_LABEL = KIND_LABEL;
|
|
1894
2367
|
exports.MARKER = MARKER;
|
|
1895
2368
|
exports.PROTOCOL_VERSION = PROTOCOL_VERSION;
|
|
1896
2369
|
exports.VERSION = VERSION;
|
|
2370
|
+
exports.analyzeCommit = analyzeCommit;
|
|
1897
2371
|
exports.assertWithinBudget = assertWithinBudget;
|
|
1898
2372
|
exports.avoidableCounts = avoidableCounts;
|
|
1899
2373
|
exports.buildReport = buildReport;
|
|
2374
|
+
exports.cascadeTree = cascadeTree;
|
|
1900
2375
|
exports.checkBudget = checkBudget;
|
|
1901
2376
|
exports.classify = classify;
|
|
1902
2377
|
exports.combineNotifiers = combineNotifiers;
|
|
2378
|
+
exports.compareSessions = compareSessions;
|
|
1903
2379
|
exports.compareSummaries = compareSummaries;
|
|
1904
2380
|
exports.configure = configure;
|
|
2381
|
+
exports.contextAttribution = contextAttribution;
|
|
1905
2382
|
exports.createCollector = createCollector;
|
|
1906
2383
|
exports.createDevtoolsNotifier = createDevtoolsNotifier;
|
|
1907
2384
|
exports.customHooksFromStack = customHooksFromStack;
|
|
1908
2385
|
exports.deepEqual = deepEqual;
|
|
1909
2386
|
exports.deserializeOptions = deserializeOptions;
|
|
2387
|
+
exports.diffLeaves = diffLeaves;
|
|
1910
2388
|
exports.diffRecords = diffRecords;
|
|
1911
2389
|
exports.disable = disable;
|
|
1912
2390
|
exports.ensureDevtoolsHook = ensureDevtoolsHook;
|
|
2391
|
+
exports.explainTracking = explainTracking;
|
|
2392
|
+
exports.firstDifferentPath = firstDifferentPath;
|
|
1913
2393
|
exports.fixKey = fixKey;
|
|
1914
2394
|
exports.fixesFor = fixesFor;
|
|
1915
2395
|
exports.formatComparison = formatComparison;
|
|
1916
2396
|
exports.formatFixes = formatFixes;
|
|
2397
|
+
exports.formatRootCauses = formatRootCauses;
|
|
1917
2398
|
exports.getDisplayName = getDisplayName;
|
|
1918
2399
|
exports.getRenderers = getRenderers;
|
|
2400
|
+
exports.groupByCommit = groupByCommit;
|
|
1919
2401
|
exports.hookLabel = hookLabel;
|
|
2402
|
+
exports.indexByComponent = indexByComponent;
|
|
1920
2403
|
exports.init = init;
|
|
1921
2404
|
exports.isEnabled = isEnabled;
|
|
1922
2405
|
exports.isProductionReact = isProductionReact;
|
|
1923
2406
|
exports.parseExport = parseExport;
|
|
1924
2407
|
exports.printReport = printReport;
|
|
1925
2408
|
exports.rankFixes = rankFixes;
|
|
2409
|
+
exports.rankRootCauses = rankRootCauses;
|
|
1926
2410
|
exports.resolveHookNames = resolveHookNames;
|
|
2411
|
+
exports.rootCauseOf = rootCauseOf;
|
|
2412
|
+
exports.rootCauseSummary = rootCauseSummary;
|
|
1927
2413
|
exports.serialize = serialize;
|
|
1928
2414
|
exports.serializeOptions = serializeOptions;
|
|
2415
|
+
exports.shortValue = shortValue;
|
|
1929
2416
|
exports.shouldTrack = shouldTrack;
|
|
1930
2417
|
exports.storeAdvice = storeAdvice;
|
|
1931
2418
|
exports.summarize = summarize;
|
|
1932
2419
|
exports.summarizeReports = summarizeReports;
|
|
2420
|
+
exports.summarizeSession = summarizeSession;
|
|
1933
2421
|
exports.toBudget = toBudget;
|
|
1934
2422
|
exports.track = track;
|
|
1935
2423
|
exports.useWhyRerender = useWhyRerender;
|