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.js
CHANGED
|
@@ -125,6 +125,7 @@ function deepEqualObjects(a, b, seen) {
|
|
|
125
125
|
}
|
|
126
126
|
return false;
|
|
127
127
|
}
|
|
128
|
+
var AVOIDABLE_KINDS = /* @__PURE__ */ new Set(["deep-equal", "function", "element"]);
|
|
128
129
|
function classify(prev, next) {
|
|
129
130
|
if (typeof prev === "function" && typeof next === "function") {
|
|
130
131
|
return prev.name === next.name && prev.toString() === next.toString() ? "function" : "different";
|
|
@@ -161,30 +162,47 @@ function diffRecords(prev, next, basePath = "") {
|
|
|
161
162
|
if (Object.is(a, b)) continue;
|
|
162
163
|
const kind = classify(a, b);
|
|
163
164
|
if (kind === "different") {
|
|
164
|
-
changes.push({ path: diffBudgetExhausted() ? path : firstDifferentPath(a, b, path), kind, prev: a, next: b });
|
|
165
|
+
changes.push({ path: diffBudgetExhausted() ? path : firstDifferentPath(a, b, path) ?? path, kind, prev: a, next: b });
|
|
165
166
|
} else {
|
|
166
167
|
changes.push({ path, kind, prev: a, next: b });
|
|
167
168
|
}
|
|
168
169
|
}
|
|
169
170
|
return changes;
|
|
170
171
|
}
|
|
171
|
-
function firstDifferentPath(a, b, path, depth = 0) {
|
|
172
|
-
if (depth > 8) return path;
|
|
172
|
+
function firstDifferentPath(a, b, path = "", depth = 0) {
|
|
173
|
+
if (depth > 8) return path || "(value)";
|
|
173
174
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
174
|
-
if (a.length !== b.length) return `${path}.length
|
|
175
|
+
if (a.length !== b.length) return path ? `${path}.length` : "length";
|
|
175
176
|
for (let i = 0; i < a.length; i++) {
|
|
176
177
|
if (!deepEqual(a[i], b[i])) return firstDifferentPath(a[i], b[i], joinPath(path, i), depth + 1);
|
|
177
178
|
}
|
|
178
|
-
return
|
|
179
|
+
return null;
|
|
179
180
|
}
|
|
180
181
|
if (isPlainObject(a) && isPlainObject(b)) {
|
|
181
182
|
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
182
183
|
for (const k of keys) {
|
|
183
184
|
if (!deepEqual(a[k], b[k])) return firstDifferentPath(a[k], b[k], joinPath(path, k), depth + 1);
|
|
184
185
|
}
|
|
185
|
-
return
|
|
186
|
+
return null;
|
|
186
187
|
}
|
|
187
|
-
return path;
|
|
188
|
+
return deepEqual(a, b) ? null : path || "(value)";
|
|
189
|
+
}
|
|
190
|
+
function diffLeaves(a, b, limit = 20, path = "", out = []) {
|
|
191
|
+
if (out.length >= limit || Object.is(a, b)) return out;
|
|
192
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
193
|
+
const n = Math.max(a.length, b.length);
|
|
194
|
+
for (let i = 0; i < n && out.length < limit; i++) diffLeaves(a[i], b[i], limit, joinPath(path, i), out);
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
198
|
+
for (const k of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) {
|
|
199
|
+
if (out.length >= limit) break;
|
|
200
|
+
diffLeaves(a[k], b[k], limit, joinPath(path, k), out);
|
|
201
|
+
}
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
if (!deepEqual(a, b)) out.push({ path: path || "(value)", prev: a, next: b });
|
|
205
|
+
return out;
|
|
188
206
|
}
|
|
189
207
|
|
|
190
208
|
// src/report.ts
|
|
@@ -202,10 +220,12 @@ function storeAdvice(custom) {
|
|
|
202
220
|
}
|
|
203
221
|
var isGenuine = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
|
|
204
222
|
var isChildren = (change) => change.path === "children";
|
|
223
|
+
var isRefObject = (change) => change.path === "ref" && change.kind === "deep-equal";
|
|
205
224
|
function fixFor(change) {
|
|
206
225
|
if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
|
|
207
226
|
return `lift the children out of the parent's render: memoize them with useMemo, hoist static elements to module scope, or render them from a component that does not re-render`;
|
|
208
227
|
}
|
|
228
|
+
if (isRefObject(change)) return `create the ref once with useRef (or createRef outside the render) and pass the same object every time`;
|
|
209
229
|
switch (change.kind) {
|
|
210
230
|
case "function":
|
|
211
231
|
return `wrap it in useCallback (or hoist it out of the parent's render)`;
|
|
@@ -221,6 +241,7 @@ function describe(change) {
|
|
|
221
241
|
if (isChildren(change) && (change.kind === "element" || change.kind === "deep-equal")) {
|
|
222
242
|
return `children are new React elements with the same types and props on every render of the parent`;
|
|
223
243
|
}
|
|
244
|
+
if (isRefObject(change)) return `prop "ref" is a new ref object on every render (React re-attaches it each time)`;
|
|
224
245
|
switch (change.kind) {
|
|
225
246
|
case "deep-equal":
|
|
226
247
|
return `prop "${change.path}" is a new reference but deep-equal to the previous value`;
|
|
@@ -248,11 +269,21 @@ function buildReport(input) {
|
|
|
248
269
|
if (causes.length === 0) trigger = "parent";
|
|
249
270
|
else if (causes.length === 1) trigger = causes[0];
|
|
250
271
|
else trigger = "mixed";
|
|
251
|
-
const
|
|
272
|
+
const nothingChanged = input.propChanges.length === 0 && stateChanges.length === 0 && hookChanges.length === 0;
|
|
273
|
+
const cacheHit = input.compiled === true && trigger === "parent" && input.propChanges.length === 0;
|
|
274
|
+
const revealed = input.revealed === true && trigger === "parent";
|
|
275
|
+
const avoidable = trigger === "parent" && !cacheHit && !revealed;
|
|
252
276
|
const reasons = [];
|
|
253
|
-
|
|
277
|
+
const memoFix = input.classComponent ? `Make "${input.component}" extend PureComponent (or implement shouldComponentUpdate)` : `Wrap "${input.component}" in React.memo (or extend PureComponent)`;
|
|
278
|
+
if (cacheHit) {
|
|
279
|
+
reasons.push(`compiled by React Compiler: its output is memoized and the inputs did not change, so this render is cheap; nothing to fix.`);
|
|
280
|
+
} else if (revealed) {
|
|
281
|
+
reasons.push(
|
|
282
|
+
`re-rendered because the Suspense boundary above it switched from its fallback to content: React re-renders the content it kept hidden while the fallback was shown. A one-off; nothing to fix.`
|
|
283
|
+
);
|
|
284
|
+
} else if (avoidable && nothingChanged) {
|
|
254
285
|
const who = input.parent ? `<${input.parent.name}> re-rendered (${describeTrigger(input.parent.trigger)})` : "its parent re-rendered";
|
|
255
|
-
reasons.push(`re-rendered with identical props because ${who}.
|
|
286
|
+
reasons.push(`re-rendered with identical props because ${who}. ${memoFix}.`);
|
|
256
287
|
} else if (avoidable && input.parent) {
|
|
257
288
|
reasons.push(`caused by <${input.parent.name}> re-rendering (${describeTrigger(input.parent.trigger)}).`);
|
|
258
289
|
}
|
|
@@ -261,9 +292,9 @@ function buildReport(input) {
|
|
|
261
292
|
reasons.push(fix ? `${describe(c)}: ${fix}.` : `${describe(c)}.`);
|
|
262
293
|
}
|
|
263
294
|
const memoized = input.memoized !== false;
|
|
264
|
-
if (avoidable && !memoized && input.propChanges.length > 0) {
|
|
295
|
+
if (avoidable && !memoized && !input.compiled && input.propChanges.length > 0) {
|
|
265
296
|
reasons.push(
|
|
266
|
-
`"${input.component}" is not memoized, so fixing the props alone will not stop this re-render: also wrap it in React.memo (or extend PureComponent).`
|
|
297
|
+
`"${input.component}" is not memoized, so fixing the props alone will not stop this re-render: also ${input.classComponent ? "extend PureComponent (or implement shouldComponentUpdate)" : "wrap it in React.memo (or extend PureComponent)"}.`
|
|
267
298
|
);
|
|
268
299
|
}
|
|
269
300
|
for (const c of stateChanges) {
|
|
@@ -275,6 +306,8 @@ function buildReport(input) {
|
|
|
275
306
|
const where = c.provider && c.provider.component ? ` (provided by <${c.provider.component}>)` : "";
|
|
276
307
|
const keys = c.changedKeys && typeof c.totalKeys === "number" && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys ? `: only ${c.changedKeys.map((k) => `"${k}"`).join(", ")} of ${c.totalKeys} keys changed, yet every consumer re-renders. Split the context or memoize the slices consumers read` : "";
|
|
277
308
|
reasons.push(`${c.path} changed${where}${keys}.`);
|
|
309
|
+
} else if (c.hook === "useDeferredValue" && isGenuine(c)) {
|
|
310
|
+
reasons.push(`${hookLabel(c)} caught up with its latest value: this is the deferred second render React schedules after the urgent one; nothing to fix.`);
|
|
278
311
|
} else if (isGenuine(c)) reasons.push(`${hookLabel(c)} changed.`);
|
|
279
312
|
else if (isStateHook(c))
|
|
280
313
|
reasons.push(`${hookLabel(c)} was set to a value deep-equal to the current one (new reference, same contents): reuse the existing object or bail out before calling the setter.`);
|
|
@@ -305,6 +338,7 @@ function buildReport(input) {
|
|
|
305
338
|
reasons,
|
|
306
339
|
time: now()
|
|
307
340
|
};
|
|
341
|
+
if (input.compiled) report.compiled = true;
|
|
308
342
|
if (input.selfDuration !== void 0) report.selfDuration = input.selfDuration;
|
|
309
343
|
if (input.treeDuration !== void 0) report.treeDuration = input.treeDuration;
|
|
310
344
|
if (input.commitPriority) report.commitPriority = input.commitPriority;
|
|
@@ -342,10 +376,10 @@ var KIND_LABEL = {
|
|
|
342
376
|
};
|
|
343
377
|
function summarize(report) {
|
|
344
378
|
const counts2 = /* @__PURE__ */ new Map();
|
|
345
|
-
for (const c of [...report.propChanges, ...report.stateChanges, ...report.hookChanges]) {
|
|
379
|
+
for (const c of [...report.propChanges || [], ...report.stateChanges || [], ...report.hookChanges || []]) {
|
|
346
380
|
counts2.set(c.kind, (counts2.get(c.kind) ?? 0) + 1);
|
|
347
381
|
}
|
|
348
|
-
const parts = [...counts2].map(([kind, n]) => `${n} ${KIND_LABEL[kind]}`);
|
|
382
|
+
const parts = [...counts2].map(([kind, n]) => `${n} ${KIND_LABEL[kind] || kind}`);
|
|
349
383
|
if (parts.length === 0) parts.push("no changes");
|
|
350
384
|
return parts.join(", ");
|
|
351
385
|
}
|
|
@@ -367,16 +401,27 @@ function printReport(report, options) {
|
|
|
367
401
|
}
|
|
368
402
|
|
|
369
403
|
// src/state.ts
|
|
404
|
+
var MAX_SEEN_COMPONENTS = 500;
|
|
370
405
|
var KEY = /* @__PURE__ */ Symbol.for("rerender-lens.state");
|
|
371
406
|
function getState() {
|
|
372
407
|
const g = globalThis;
|
|
373
408
|
let s = g[KEY];
|
|
374
409
|
if (!s) {
|
|
375
|
-
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 };
|
|
410
|
+
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 };
|
|
376
411
|
g[KEY] = s;
|
|
377
412
|
}
|
|
378
413
|
return s;
|
|
379
414
|
}
|
|
415
|
+
function noteRendered(s, name, tracked) {
|
|
416
|
+
if (!s.seen.has(name)) {
|
|
417
|
+
if (s.seen.size >= MAX_SEEN_COMPONENTS) {
|
|
418
|
+
s.seenOverflow = true;
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
s.seen.add(name);
|
|
422
|
+
}
|
|
423
|
+
if (tracked) s.seenTracked.add(name);
|
|
424
|
+
}
|
|
380
425
|
function warnOnce(key, message) {
|
|
381
426
|
const s = getState();
|
|
382
427
|
if (s.warnedOnce.has(key)) return;
|
|
@@ -408,6 +453,27 @@ function dispatch(report, override) {
|
|
|
408
453
|
}
|
|
409
454
|
|
|
410
455
|
// src/hookNames.ts
|
|
456
|
+
var NODE_COUNT = {
|
|
457
|
+
useState: 1,
|
|
458
|
+
useReducer: 1,
|
|
459
|
+
useRef: 1,
|
|
460
|
+
useMemo: 1,
|
|
461
|
+
useCallback: 1,
|
|
462
|
+
useEffect: 1,
|
|
463
|
+
useLayoutEffect: 1,
|
|
464
|
+
useInsertionEffect: 1,
|
|
465
|
+
useImperativeHandle: 1,
|
|
466
|
+
useSyncExternalStore: 2,
|
|
467
|
+
useTransition: 2,
|
|
468
|
+
useDeferredValue: 1,
|
|
469
|
+
useId: 1,
|
|
470
|
+
useOptimistic: 1,
|
|
471
|
+
useActionState: 3,
|
|
472
|
+
useFormState: 3,
|
|
473
|
+
useEffectEvent: 1,
|
|
474
|
+
useCacheRefresh: 1
|
|
475
|
+
};
|
|
476
|
+
var BUILTIN_HOOKS = /* @__PURE__ */ new Set([...Object.keys(NODE_COUNT), "useContext", "useDebugValue", "use"]);
|
|
411
477
|
var noop = () => {
|
|
412
478
|
};
|
|
413
479
|
function customHooksFromStack(stack, componentName, marker) {
|
|
@@ -422,7 +488,7 @@ function customHooksFromStack(stack, componentName, marker) {
|
|
|
422
488
|
continue;
|
|
423
489
|
}
|
|
424
490
|
if (name === componentName || name.endsWith("." + componentName)) break;
|
|
425
|
-
if (/^use[A-Z0-9_]/.test(name)) names.push(name);
|
|
491
|
+
if (/^use[A-Z0-9_]/.test(name) && !BUILTIN_HOOKS.has(name)) names.push(name);
|
|
426
492
|
if (names.length > 8) break;
|
|
427
493
|
}
|
|
428
494
|
return names;
|
|
@@ -722,6 +788,11 @@ function isMemoizedFiber(fiber) {
|
|
|
722
788
|
}
|
|
723
789
|
return false;
|
|
724
790
|
}
|
|
791
|
+
function isCompiledFiber(fiber) {
|
|
792
|
+
if (fiber.tag === ClassComponent) return false;
|
|
793
|
+
const cache2 = fiber.updateQueue?.memoCache;
|
|
794
|
+
return !!cache2 && Array.isArray(cache2.data);
|
|
795
|
+
}
|
|
725
796
|
function durationsOf(fiber) {
|
|
726
797
|
if (typeof fiber.actualDuration !== "number") return null;
|
|
727
798
|
const tree = fiber.actualDuration;
|
|
@@ -900,7 +971,8 @@ function diffHooks(fiber, alt) {
|
|
|
900
971
|
const labels = hookLabelsFor(fiber, b);
|
|
901
972
|
let i = 0;
|
|
902
973
|
while (a && b) {
|
|
903
|
-
|
|
974
|
+
const deferred = labels?.[i] === "useDeferredValue";
|
|
975
|
+
if ((deferred || isStateNode(b) || isStoreNode(b)) && !Object.is(a.memoizedState, b.memoizedState)) {
|
|
904
976
|
const hook = labels?.[i] ?? hookLabel2(b);
|
|
905
977
|
out.push({
|
|
906
978
|
path: `${hook}#${i}`,
|
|
@@ -934,6 +1006,10 @@ function diffContexts(fiber, alt) {
|
|
|
934
1006
|
let a = alt.dependencies?.firstContext ?? null;
|
|
935
1007
|
let b = fiber.dependencies?.firstContext ?? null;
|
|
936
1008
|
let i = 0;
|
|
1009
|
+
if (b && !("memoizedValue" in b)) {
|
|
1010
|
+
warnOnce("contextValues", "this React does not record context values on the fiber (React < 18): context changes are reported as plain parent re-renders.");
|
|
1011
|
+
return out;
|
|
1012
|
+
}
|
|
937
1013
|
while (a && b) {
|
|
938
1014
|
if (a.context === b.context && !Object.is(a.memoizedValue, b.memoizedValue)) {
|
|
939
1015
|
const name = b.context.displayName ?? "Context";
|
|
@@ -966,10 +1042,26 @@ function diffContexts(fiber, alt) {
|
|
|
966
1042
|
return out;
|
|
967
1043
|
}
|
|
968
1044
|
var isGenuine2 = (c) => c.kind === "different" || c.kind === "added" || c.kind === "removed";
|
|
1045
|
+
var isRefObject2 = (v) => {
|
|
1046
|
+
if (typeof v !== "object" || v === null) return false;
|
|
1047
|
+
const keys = Object.keys(v);
|
|
1048
|
+
return keys.length === 1 && keys[0] === "current";
|
|
1049
|
+
};
|
|
1050
|
+
function diffProps(prev, next) {
|
|
1051
|
+
if (prev === next) return [];
|
|
1052
|
+
const pr = prev.ref;
|
|
1053
|
+
const nr = next.ref;
|
|
1054
|
+
if (pr !== nr && isRefObject2(pr) && isRefObject2(nr)) {
|
|
1055
|
+
const { ref: _p, ...restPrev } = prev;
|
|
1056
|
+
const { ref: _n, ...restNext } = next;
|
|
1057
|
+
return [{ path: "ref", kind: "deep-equal", prev: pr, next: nr }, ...diffRecords(restPrev, restNext)];
|
|
1058
|
+
}
|
|
1059
|
+
return diffRecords(prev, next);
|
|
1060
|
+
}
|
|
969
1061
|
function analyze(fiber, alt, trackHooks) {
|
|
970
1062
|
const prevProps = alt.memoizedProps ?? {};
|
|
971
1063
|
const nextProps = fiber.memoizedProps ?? {};
|
|
972
|
-
const propChanges =
|
|
1064
|
+
const propChanges = diffProps(prevProps, nextProps);
|
|
973
1065
|
let stateChanges = [];
|
|
974
1066
|
let hookChanges = [];
|
|
975
1067
|
if (fiber.tag === ClassComponent) {
|
|
@@ -982,7 +1074,7 @@ function analyze(fiber, alt, trackHooks) {
|
|
|
982
1074
|
} else if (trackHooks) {
|
|
983
1075
|
hookChanges = [...diffHooks(fiber, alt), ...diffContexts(fiber, alt)];
|
|
984
1076
|
}
|
|
985
|
-
const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore";
|
|
1077
|
+
const isStateHook = (h) => h.hook !== "useContext" && h.hook !== "useSyncExternalStore" && h.hook !== "useDeferredValue";
|
|
986
1078
|
const causes = [];
|
|
987
1079
|
if (propChanges.some(isGenuine2)) causes.push("props");
|
|
988
1080
|
if (stateChanges.some(isGenuine2) || hookChanges.some((h) => isStateHook(h) && isGenuine2(h))) causes.push("state");
|
|
@@ -1022,6 +1114,24 @@ var lastCommit = null;
|
|
|
1022
1114
|
var EFFECT_LOOP_WINDOW_MS = 50;
|
|
1023
1115
|
var MAX_REPORTS_PER_COMMIT = 200;
|
|
1024
1116
|
var COMMIT_TIME_BUDGET_MS = 25;
|
|
1117
|
+
function isDeferredCatchUp(fiber) {
|
|
1118
|
+
const types = fiber._debugHookTypes;
|
|
1119
|
+
if (!types || !types.includes("useDeferredValue") || !fiber.alternate) return false;
|
|
1120
|
+
const first = fiber.memoizedState;
|
|
1121
|
+
if (!isHookList(first)) return false;
|
|
1122
|
+
const labels = hookLabelsFor(fiber, first);
|
|
1123
|
+
if (!labels) return false;
|
|
1124
|
+
let a = fiber.alternate.memoizedState;
|
|
1125
|
+
let b = first;
|
|
1126
|
+
for (let i = 0; a && b; a = a.next, b = b.next, i++) {
|
|
1127
|
+
if (labels[i] === "useDeferredValue" && !Object.is(a.memoizedState, b.memoizedState)) return true;
|
|
1128
|
+
}
|
|
1129
|
+
return false;
|
|
1130
|
+
}
|
|
1131
|
+
function isUnder(fiber, boundaries) {
|
|
1132
|
+
for (let f = fiber.return; f && f.tag !== HostRoot; f = f.return) if (f.tag === SuspenseComponent && boundaries.includes(f)) return true;
|
|
1133
|
+
return false;
|
|
1134
|
+
}
|
|
1025
1135
|
function onCommit(root, commitPriority) {
|
|
1026
1136
|
const s = getState();
|
|
1027
1137
|
if (!s.enabled) return;
|
|
@@ -1031,17 +1141,20 @@ function onCommit(root, commitPriority) {
|
|
|
1031
1141
|
const rendered = [];
|
|
1032
1142
|
const renderedNames = /* @__PURE__ */ new Set();
|
|
1033
1143
|
let hot = false;
|
|
1034
|
-
let
|
|
1144
|
+
let resolvedBoundaries = null;
|
|
1035
1145
|
const stack = [root.current];
|
|
1036
1146
|
while (stack.length) {
|
|
1037
1147
|
const fiber = stack.pop();
|
|
1038
1148
|
const alt = fiber.alternate;
|
|
1039
1149
|
if (alt && isComponentTag(fiber.tag) && didRender(fiber)) {
|
|
1040
1150
|
if (isHotSwapped(fiber, alt)) hot = true;
|
|
1041
|
-
|
|
1042
|
-
|
|
1151
|
+
const name = fiberName(fiber);
|
|
1152
|
+
renderedNames.add(name);
|
|
1153
|
+
const tracked = shouldTrack(fiberType(fiber), o);
|
|
1154
|
+
noteRendered(s, name, tracked);
|
|
1155
|
+
if (tracked) rendered.push(fiber);
|
|
1043
1156
|
}
|
|
1044
|
-
if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null)
|
|
1157
|
+
if (fiber.tag === SuspenseComponent && alt && alt.memoizedState !== null && fiber.memoizedState === null) (resolvedBoundaries ?? (resolvedBoundaries = [])).push(fiber);
|
|
1045
1158
|
if (!alt || fiber.child !== alt.child) {
|
|
1046
1159
|
for (let c = fiber.child; c; c = c.sibling) stack.push(c);
|
|
1047
1160
|
}
|
|
@@ -1053,10 +1166,10 @@ function onCommit(root, commitPriority) {
|
|
|
1053
1166
|
let commitCause;
|
|
1054
1167
|
let afterCommit;
|
|
1055
1168
|
const inputDriven = commitPriority === "immediate" || commitPriority === "user-blocking";
|
|
1056
|
-
if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u))) {
|
|
1169
|
+
if (!inputDriven && lastCommit && at - lastCommit.at < EFFECT_LOOP_WINDOW_MS && updaters.some((u) => lastCommit.rendered.has(u)) && !rendered.some(isDeferredCatchUp)) {
|
|
1057
1170
|
commitCause = "effect-after-commit";
|
|
1058
1171
|
afterCommit = lastCommit.id;
|
|
1059
|
-
} else if (
|
|
1172
|
+
} else if (resolvedBoundaries) commitCause = "suspense-resolved";
|
|
1060
1173
|
if (rendered.length === 0) {
|
|
1061
1174
|
lastCommit = { id: s.nextCommitId, at, rendered: renderedNames };
|
|
1062
1175
|
return;
|
|
@@ -1108,6 +1221,9 @@ function onCommit(root, commitPriority) {
|
|
|
1108
1221
|
owner: ownerName(fiber),
|
|
1109
1222
|
path: componentPath(fiber),
|
|
1110
1223
|
memoized: isMemoizedFiber(fiber),
|
|
1224
|
+
compiled: isCompiledFiber(fiber),
|
|
1225
|
+
classComponent: fiber.tag === ClassComponent,
|
|
1226
|
+
revealed: resolvedBoundaries !== null && a.trigger === "parent" && isUnder(fiber, resolvedBoundaries),
|
|
1111
1227
|
selfDuration: durations ? durations.self : void 0,
|
|
1112
1228
|
treeDuration: durations ? durations.tree : void 0,
|
|
1113
1229
|
commitId,
|
|
@@ -1161,10 +1277,53 @@ function shouldTrack(type, o) {
|
|
|
1161
1277
|
if (o.trackAllMemoized) return isMemo(type) || isPureClass(type);
|
|
1162
1278
|
return false;
|
|
1163
1279
|
}
|
|
1280
|
+
var ANONYMOUS = "Anonymous";
|
|
1281
|
+
function explainTracking(type, o) {
|
|
1282
|
+
if (typeof type === "string")
|
|
1283
|
+
return { tracked: false, name: type, memoized: false, reason: `<${type}> is a DOM element, not a React component; only components are tracked.` };
|
|
1284
|
+
if (!isComponentLike(type))
|
|
1285
|
+
return { tracked: false, name: ANONYMOUS, memoized: false, reason: "This is not a React component type, so it is never tracked." };
|
|
1286
|
+
const name = getDisplayName(type);
|
|
1287
|
+
const memo = isMemo(type);
|
|
1288
|
+
const memoized = memo || isPureClass(type);
|
|
1289
|
+
if (o.exclude?.some((m) => matches(m, name)))
|
|
1290
|
+
return { tracked: false, name, memoized, reason: `<${name}> matches "exclude", which wins over every other rule.`, fix: `Remove "${name}" from the exclude list.` };
|
|
1291
|
+
if (hasMarker(type)) return { tracked: true, name, memoized, reason: `<${name}> is marked with track() (the ${MARKER} static).` };
|
|
1292
|
+
if (o.include?.some((m) => matches(m, name))) return { tracked: true, name, memoized, reason: `<${name}> matches "include".` };
|
|
1293
|
+
if (o.trackAllComponents) return { tracked: true, name, memoized, reason: '"Track every component" is on, so every component is tracked.' };
|
|
1294
|
+
if (o.trackAllMemoized && memoized)
|
|
1295
|
+
return { tracked: true, name, memoized, reason: `<${name}> is ${memo ? "a React.memo component" : "a PureComponent"}, and "Track every React.memo and PureComponent" is on.` };
|
|
1296
|
+
if (name === ANONYMOUS)
|
|
1297
|
+
return {
|
|
1298
|
+
tracked: false,
|
|
1299
|
+
name,
|
|
1300
|
+
memoized,
|
|
1301
|
+
reason: 'This component has no display name, so "include" and "exclude" cannot address it.',
|
|
1302
|
+
fix: "Give the function a name, set displayName, or mark it with track(fn, 'Name')."
|
|
1303
|
+
};
|
|
1304
|
+
if (o.trackAllMemoized)
|
|
1305
|
+
return {
|
|
1306
|
+
tracked: false,
|
|
1307
|
+
name,
|
|
1308
|
+
memoized,
|
|
1309
|
+
reason: `"Track every React.memo and PureComponent" only covers React.memo components and PureComponent classes, and <${name}> is neither.`,
|
|
1310
|
+
fix: `Add "${name}" to include, wrap it in React.memo, or turn on "Track every component".`
|
|
1311
|
+
};
|
|
1312
|
+
return {
|
|
1313
|
+
tracked: false,
|
|
1314
|
+
name,
|
|
1315
|
+
memoized,
|
|
1316
|
+
reason: `Nothing selects <${name}>: it is not marked with track(), no "include" pattern matches it, and neither "track every" option is on.`,
|
|
1317
|
+
fix: `Add "${name}" to include, or turn on "Track every React.memo and PureComponent" / "Track every component".`
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1164
1320
|
function init(options = {}) {
|
|
1165
1321
|
const s = getState();
|
|
1166
1322
|
s.options = { ...options };
|
|
1167
1323
|
s.printed.clear();
|
|
1324
|
+
s.seen.clear();
|
|
1325
|
+
s.seenTracked.clear();
|
|
1326
|
+
s.seenOverflow = false;
|
|
1168
1327
|
if (!s.enabled) {
|
|
1169
1328
|
s.detach = attach();
|
|
1170
1329
|
s.enabled = true;
|
|
@@ -1219,38 +1378,270 @@ function useWhyRerender(name, values, options) {
|
|
|
1219
1378
|
});
|
|
1220
1379
|
}
|
|
1221
1380
|
|
|
1381
|
+
// src/causes.ts
|
|
1382
|
+
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);
|
|
1383
|
+
var bump = (m, key, n = 1) => void m.set(key, (m.get(key) || 0) + n);
|
|
1384
|
+
function indexByComponent(reports) {
|
|
1385
|
+
const index = /* @__PURE__ */ new Map();
|
|
1386
|
+
for (const r of reports) {
|
|
1387
|
+
const list = index.get(r.component);
|
|
1388
|
+
if (list) list.push(r);
|
|
1389
|
+
else index.set(r.component, [r]);
|
|
1390
|
+
}
|
|
1391
|
+
return index;
|
|
1392
|
+
}
|
|
1393
|
+
function rootCauseOf(r, commitReports, index = indexByComponent(commitReports)) {
|
|
1394
|
+
let cur = r;
|
|
1395
|
+
const seen = /* @__PURE__ */ new Set([r]);
|
|
1396
|
+
while (cur.trigger === "parent" && cur.parent) {
|
|
1397
|
+
const parent = cur.parent;
|
|
1398
|
+
const candidates = index.get(parent.name);
|
|
1399
|
+
const p = candidates && candidates.find((x) => isAncestorReport(x, cur));
|
|
1400
|
+
if (!p || seen.has(p)) return { name: parent.name, trigger: parent.trigger, report: null };
|
|
1401
|
+
seen.add(p);
|
|
1402
|
+
cur = p;
|
|
1403
|
+
}
|
|
1404
|
+
return cur === r ? null : { name: cur.component, trigger: cur.trigger, report: cur };
|
|
1405
|
+
}
|
|
1406
|
+
function rootCausesOf(reports) {
|
|
1407
|
+
const roots = /* @__PURE__ */ new Map();
|
|
1408
|
+
const rootByReport = /* @__PURE__ */ new Map();
|
|
1409
|
+
const index = indexByComponent(reports);
|
|
1410
|
+
let avoidable = 0;
|
|
1411
|
+
let wasted = 0;
|
|
1412
|
+
for (const r of reports) {
|
|
1413
|
+
if (!r.avoidable) continue;
|
|
1414
|
+
avoidable++;
|
|
1415
|
+
if (typeof r.selfDuration === "number") wasted += r.selfDuration;
|
|
1416
|
+
const root = rootCauseOf(r, reports, index);
|
|
1417
|
+
const name = root ? root.name : r.parent && r.parent.name || "(unknown)";
|
|
1418
|
+
const trigger = root ? root.trigger : r.parent && r.parent.trigger || "parent";
|
|
1419
|
+
rootByReport.set(r, name);
|
|
1420
|
+
let agg = roots.get(name);
|
|
1421
|
+
if (!agg) {
|
|
1422
|
+
agg = { name, trigger, count: 0, components: /* @__PURE__ */ new Map() };
|
|
1423
|
+
roots.set(name, agg);
|
|
1424
|
+
}
|
|
1425
|
+
agg.count++;
|
|
1426
|
+
bump(agg.components, r.component);
|
|
1427
|
+
}
|
|
1428
|
+
return { roots: [...roots.values()].sort((a, b) => b.count - a.count), rootByReport, avoidable, wasted };
|
|
1429
|
+
}
|
|
1430
|
+
function analyzeCommit(reports) {
|
|
1431
|
+
const first = reports[0];
|
|
1432
|
+
return {
|
|
1433
|
+
id: first && typeof first.commitId === "number" ? first.commitId : 0,
|
|
1434
|
+
total: reports.length,
|
|
1435
|
+
...rootCausesOf(reports),
|
|
1436
|
+
contexts: contextAttribution(reports),
|
|
1437
|
+
reports
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
function contextAttribution(reports) {
|
|
1441
|
+
const byCtx = /* @__PURE__ */ new Map();
|
|
1442
|
+
for (const r of reports) {
|
|
1443
|
+
for (const c of r.hookChanges || []) {
|
|
1444
|
+
if (c.hook !== "useContext" && !/^useContext/.test(c.path)) continue;
|
|
1445
|
+
const m = /useContext\((.*)\)/.exec(c.path);
|
|
1446
|
+
const name = m && m[1] ? m[1] : c.path;
|
|
1447
|
+
let agg = byCtx.get(name);
|
|
1448
|
+
if (!agg) {
|
|
1449
|
+
agg = { name, consumers: 0, avoidable: 0, components: /* @__PURE__ */ new Map(), commits: /* @__PURE__ */ new Set(), providers: /* @__PURE__ */ new Map(), changedKeys: /* @__PURE__ */ new Set(), totalKeys: 0 };
|
|
1450
|
+
byCtx.set(name, agg);
|
|
1451
|
+
}
|
|
1452
|
+
agg.consumers++;
|
|
1453
|
+
if (AVOIDABLE_KINDS.has(c.kind)) agg.avoidable++;
|
|
1454
|
+
bump(agg.components, r.component);
|
|
1455
|
+
agg.commits.add(typeof r.commitId === "number" ? r.commitId : 0);
|
|
1456
|
+
if (c.provider && c.provider.component) bump(agg.providers, c.provider.component);
|
|
1457
|
+
if (c.changedKeys) for (const k of c.changedKeys) agg.changedKeys.add(k);
|
|
1458
|
+
if (typeof c.totalKeys === "number") agg.totalKeys = Math.max(agg.totalKeys, c.totalKeys);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
return [...byCtx.values()].sort((a, b) => b.consumers - a.consumers);
|
|
1462
|
+
}
|
|
1463
|
+
function cascadeTree(reports) {
|
|
1464
|
+
const root = { name: "", children: /* @__PURE__ */ new Map(), report: null };
|
|
1465
|
+
for (const r of reports) {
|
|
1466
|
+
let node = root;
|
|
1467
|
+
for (const seg of r.path.concat([r.component])) {
|
|
1468
|
+
let next = node.children.get(seg);
|
|
1469
|
+
if (!next) {
|
|
1470
|
+
next = { name: seg, children: /* @__PURE__ */ new Map(), report: null };
|
|
1471
|
+
node.children.set(seg, next);
|
|
1472
|
+
}
|
|
1473
|
+
node = next;
|
|
1474
|
+
}
|
|
1475
|
+
if (!node.report || r.avoidable) node.report = r;
|
|
1476
|
+
node.count = (node.count || 0) + 1;
|
|
1477
|
+
if (r.avoidable) node.avoidable = (node.avoidable || 0) + 1;
|
|
1478
|
+
}
|
|
1479
|
+
return root;
|
|
1480
|
+
}
|
|
1481
|
+
function rootCauseSummary(name, commits, analyze2) {
|
|
1482
|
+
const analyzeFn = analyze2 || ((_, reports) => analyzeCommit(reports));
|
|
1483
|
+
const out = { name, trigger: "parent", commits: [], total: 0, components: /* @__PURE__ */ new Map(), affected: [] };
|
|
1484
|
+
for (const [key, reports] of commits) {
|
|
1485
|
+
const analysis = analyzeFn(key, reports);
|
|
1486
|
+
const root = analysis.roots.find((x) => x.name === name);
|
|
1487
|
+
if (!root) continue;
|
|
1488
|
+
out.trigger = root.trigger;
|
|
1489
|
+
out.commits.push({ key, analysis, count: root.count, components: root.components });
|
|
1490
|
+
out.total += root.count;
|
|
1491
|
+
for (const [c, n] of root.components) bump(out.components, c, n);
|
|
1492
|
+
for (const r of reports) if (r.avoidable && analysis.rootByReport.get(r) === name) out.affected.push(r);
|
|
1493
|
+
}
|
|
1494
|
+
out.commits.reverse();
|
|
1495
|
+
return out;
|
|
1496
|
+
}
|
|
1497
|
+
function groupByCommit(reports) {
|
|
1498
|
+
const out = /* @__PURE__ */ new Map();
|
|
1499
|
+
for (const r of reports) {
|
|
1500
|
+
if (typeof r.commitId !== "number" || r.commitId <= 0) continue;
|
|
1501
|
+
const list = out.get(r.commitId);
|
|
1502
|
+
if (list) list.push(r);
|
|
1503
|
+
else out.set(r.commitId, [r]);
|
|
1504
|
+
}
|
|
1505
|
+
return out;
|
|
1506
|
+
}
|
|
1507
|
+
function rankRootCauses(reports) {
|
|
1508
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1509
|
+
for (const commit of groupByCommit(reports).values()) {
|
|
1510
|
+
for (const root of rootCausesOf(commit).roots) {
|
|
1511
|
+
let agg = byName.get(root.name);
|
|
1512
|
+
if (!agg) {
|
|
1513
|
+
agg = { name: root.name, trigger: root.trigger, commits: 0, count: 0, components: /* @__PURE__ */ new Map() };
|
|
1514
|
+
byName.set(root.name, agg);
|
|
1515
|
+
}
|
|
1516
|
+
agg.commits++;
|
|
1517
|
+
agg.count += root.count;
|
|
1518
|
+
for (const [c, n] of root.components) bump(agg.components, c, n);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
return [...byName.values()].sort((a, b) => b.count - a.count || b.commits - a.commits || a.name.localeCompare(b.name));
|
|
1522
|
+
}
|
|
1523
|
+
var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
1524
|
+
function formatRootCauses(reports, limit = 5) {
|
|
1525
|
+
const ranked = rankRootCauses(reports);
|
|
1526
|
+
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(", ")})`);
|
|
1527
|
+
if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
|
|
1528
|
+
return lines.join("\n");
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1222
1531
|
// src/fixes.ts
|
|
1223
|
-
var
|
|
1532
|
+
var MAX_FIX_REPORTS = 50;
|
|
1224
1533
|
var rootOf = (path) => path.split(/[.[]/)[0] || path;
|
|
1534
|
+
var identifier = (name) => /^[A-Za-z_$][\w$]*$/.test(name) ? name : "value";
|
|
1535
|
+
function shortValue(v, max = 60) {
|
|
1536
|
+
let s;
|
|
1537
|
+
try {
|
|
1538
|
+
s = JSON.stringify(v);
|
|
1539
|
+
} catch {
|
|
1540
|
+
s = String(v);
|
|
1541
|
+
}
|
|
1542
|
+
if (s === void 0) s = String(v);
|
|
1543
|
+
return s.length > max ? s.slice(0, max - 3) + "..." : s;
|
|
1544
|
+
}
|
|
1225
1545
|
function fixesFor(r) {
|
|
1226
1546
|
const out = [];
|
|
1227
|
-
const
|
|
1228
|
-
const
|
|
1547
|
+
const ownerName2 = r.owner || r.parent && r.parent.name || null;
|
|
1548
|
+
const propChanges = r.propChanges || [];
|
|
1549
|
+
const changes = [...propChanges, ...r.stateChanges || [], ...r.hookChanges || []];
|
|
1229
1550
|
if (r.avoidable && (changes.length === 0 || r.memoized === false)) {
|
|
1551
|
+
const identical = changes.length === 0;
|
|
1230
1552
|
out.push({
|
|
1231
1553
|
kind: "memo",
|
|
1232
1554
|
owner: r.component,
|
|
1233
1555
|
target: r.component,
|
|
1234
1556
|
prop: null,
|
|
1235
1557
|
label: `Wrap <${r.component}> in React.memo`,
|
|
1236
|
-
detail:
|
|
1558
|
+
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.`,
|
|
1559
|
+
snippet: `// ${r.component}
|
|
1560
|
+
import { memo } from 'react';
|
|
1561
|
+
|
|
1562
|
+
export const ${r.component} = memo(function ${r.component}(props) {
|
|
1563
|
+
// ...
|
|
1564
|
+
});
|
|
1565
|
+
// class components: extend PureComponent instead`
|
|
1237
1566
|
});
|
|
1238
1567
|
}
|
|
1239
|
-
for (const c of
|
|
1240
|
-
if (!
|
|
1568
|
+
for (const c of propChanges) {
|
|
1569
|
+
if (!AVOIDABLE_KINDS.has(c.kind)) continue;
|
|
1570
|
+
const owner = ownerName2 || "?";
|
|
1241
1571
|
const root = rootOf(c.path);
|
|
1572
|
+
const id = identifier(root);
|
|
1242
1573
|
if (root === "children" && (c.kind === "element" || c.kind === "deep-equal")) {
|
|
1243
|
-
out.push({
|
|
1574
|
+
out.push({
|
|
1575
|
+
kind: "children",
|
|
1576
|
+
owner,
|
|
1577
|
+
target: r.component,
|
|
1578
|
+
prop: "children",
|
|
1579
|
+
label: `memoize children of <${r.component}> in <${owner}>`,
|
|
1580
|
+
detail: `<${owner}> re-creates the children of <${r.component}> on every render; they have the same types and props each time.`,
|
|
1581
|
+
snippet: `// ${owner}
|
|
1582
|
+
import { useMemo } from 'react';
|
|
1583
|
+
|
|
1584
|
+
const children = useMemo(() => (
|
|
1585
|
+
<>{/* the same elements */}</>
|
|
1586
|
+
), [/* deps */]);
|
|
1587
|
+
|
|
1588
|
+
<${r.component}>{children}</${r.component}>
|
|
1589
|
+
|
|
1590
|
+
// static children: hoist them to module scope
|
|
1591
|
+
const STATIC = <em>hi</em>;`
|
|
1592
|
+
});
|
|
1244
1593
|
} else if (c.kind === "function") {
|
|
1245
|
-
out.push({
|
|
1594
|
+
out.push({
|
|
1595
|
+
kind: "useCallback",
|
|
1596
|
+
owner,
|
|
1597
|
+
target: r.component,
|
|
1598
|
+
prop: root,
|
|
1599
|
+
label: `useCallback(${root}) in <${owner}>`,
|
|
1600
|
+
detail: `prop "${c.path}" of <${r.component}> is a new function on every render of <${owner}>.`,
|
|
1601
|
+
snippet: `// ${owner}
|
|
1602
|
+
import { useCallback } from 'react';
|
|
1603
|
+
|
|
1604
|
+
const ${id} = useCallback((/* args */) => {
|
|
1605
|
+
// ...
|
|
1606
|
+
}, [/* deps */]);
|
|
1607
|
+
|
|
1608
|
+
<${r.component} ${root}={${id}} />`
|
|
1609
|
+
});
|
|
1246
1610
|
} else if (c.kind === "element") {
|
|
1247
|
-
out.push({
|
|
1611
|
+
out.push({
|
|
1612
|
+
kind: "useMemoElement",
|
|
1613
|
+
owner,
|
|
1614
|
+
target: r.component,
|
|
1615
|
+
prop: root,
|
|
1616
|
+
label: `memoize element prop ${root} in <${owner}>`,
|
|
1617
|
+
detail: `prop "${c.path}" of <${r.component}> is a new element with the same type and props on every render of <${owner}>.`,
|
|
1618
|
+
snippet: `// ${owner}
|
|
1619
|
+
import { useMemo } from 'react';
|
|
1620
|
+
|
|
1621
|
+
const ${id} = useMemo(() => ${shortValue(c.next, 40)}, [/* deps */]);
|
|
1622
|
+
// or pass it as children from a component that does not re-render`
|
|
1623
|
+
});
|
|
1248
1624
|
} else {
|
|
1249
|
-
|
|
1625
|
+
const isArray = Array.isArray(c.next);
|
|
1626
|
+
out.push({
|
|
1627
|
+
kind: "useMemo",
|
|
1628
|
+
owner,
|
|
1629
|
+
target: r.component,
|
|
1630
|
+
prop: root,
|
|
1631
|
+
label: `useMemo(${root}) in <${owner}>`,
|
|
1632
|
+
detail: `prop "${c.path}" of <${r.component}> is a new ${isArray ? "array" : "object"} with the same contents on every render of <${owner}>.`,
|
|
1633
|
+
snippet: `// ${owner}
|
|
1634
|
+
import { useMemo } from 'react';
|
|
1635
|
+
|
|
1636
|
+
const ${id} = useMemo(() => (${shortValue(c.next, 80)}), [/* deps */]);
|
|
1637
|
+
|
|
1638
|
+
// or, when it never changes, hoist it to module scope:
|
|
1639
|
+
const ${id.toUpperCase()} = ${shortValue(c.next, 80)};`
|
|
1640
|
+
});
|
|
1250
1641
|
}
|
|
1251
1642
|
}
|
|
1252
|
-
for (const c of [...r.stateChanges, ...r.hookChanges]) {
|
|
1253
|
-
const isContext = c.hook === "useContext";
|
|
1643
|
+
for (const c of [...r.stateChanges || [], ...r.hookChanges || []]) {
|
|
1644
|
+
const isContext = c.hook === "useContext" || /^useContext/.test(c.path);
|
|
1254
1645
|
const ctxName = isContext ? (/useContext\((.*)\)/.exec(c.path) || [])[1] || "Context" : "";
|
|
1255
1646
|
const providerOwner = isContext && c.provider && c.provider.component ? c.provider.component : null;
|
|
1256
1647
|
if (isContext && c.kind === "different" && c.changedKeys && typeof c.totalKeys === "number" && c.changedKeys.length > 0 && c.changedKeys.length < c.totalKeys) {
|
|
@@ -1260,20 +1651,63 @@ function fixesFor(r) {
|
|
|
1260
1651
|
target: r.component,
|
|
1261
1652
|
prop: ctxName,
|
|
1262
1653
|
label: `split ${ctxName}${providerOwner ? ` in <${providerOwner}>` : ""}: only ${c.changedKeys.join(", ")} changed`,
|
|
1263
|
-
detail: `${c.changedKeys.
|
|
1654
|
+
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.`,
|
|
1655
|
+
snippet: `// ${providerOwner || "Provider"}
|
|
1656
|
+
// one context per independently-changing slice
|
|
1657
|
+
const ${identifier(ctxName)}Static = createContext(...);
|
|
1658
|
+
const ${identifier(ctxName)}${c.changedKeys.map((k) => k[0].toUpperCase() + k.slice(1)).join("")} = createContext(...);
|
|
1659
|
+
|
|
1660
|
+
// or keep one context and let consumers select a slice:
|
|
1661
|
+
const ${c.changedKeys[0]} = useContextSelector(${ctxName}, (v) => v.${c.changedKeys[0]});`
|
|
1264
1662
|
});
|
|
1265
1663
|
continue;
|
|
1266
1664
|
}
|
|
1267
|
-
if (!
|
|
1665
|
+
if (!AVOIDABLE_KINDS.has(c.kind)) continue;
|
|
1268
1666
|
if (isContext) {
|
|
1269
|
-
out.push({
|
|
1667
|
+
out.push({
|
|
1668
|
+
kind: "contextValue",
|
|
1669
|
+
owner: providerOwner || `${ctxName}.Provider`,
|
|
1670
|
+
target: r.component,
|
|
1671
|
+
prop: ctxName,
|
|
1672
|
+
label: `memoize the ${ctxName} provider value${providerOwner ? ` in <${providerOwner}>` : ""}`,
|
|
1673
|
+
detail: `<${r.component}> re-rendered because ${ctxName} produced a new value that is deep-equal to the previous one${providerOwner ? ` (Provider rendered by <${providerOwner}>)` : ""}.`,
|
|
1674
|
+
snippet: `// ${providerOwner || `where <${ctxName}.Provider> is rendered`}
|
|
1675
|
+
const value = useMemo(() => ({ /* ... */ }), [/* deps */]);
|
|
1676
|
+
<${ctxName}.Provider value={value}>`
|
|
1677
|
+
});
|
|
1270
1678
|
} else if (c.hook === "useSyncExternalStore") {
|
|
1271
1679
|
const chain = c.custom || [];
|
|
1272
1680
|
const redux = chain.some((n) => /^use(App)?Selector$/.test(n));
|
|
1273
1681
|
const zustand = !redux && chain.some((n) => /^use[A-Z]\w*Store$/.test(n) || n === "useStore" || n === "useBoundStore");
|
|
1274
|
-
|
|
1682
|
+
const via = chain.length ? ` via ${chain.join(" \u203A ")}` : "";
|
|
1683
|
+
out.push({
|
|
1684
|
+
kind: "storeSnapshot",
|
|
1685
|
+
owner: r.component,
|
|
1686
|
+
target: r.component,
|
|
1687
|
+
prop: c.path,
|
|
1688
|
+
label: redux ? `memoize the selector in <${r.component}>` : zustand ? `useShallow in <${r.component}>` : `stable getSnapshot in <${r.component}>`,
|
|
1689
|
+
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.`,
|
|
1690
|
+
snippet: redux ? `// ${r.component}
|
|
1691
|
+
import { shallowEqual } from 'react-redux';
|
|
1692
|
+
const slice = useSelector(selectSlice, shallowEqual);
|
|
1693
|
+
// or memoize: const selectSlice = createSelector([selectA, selectB], (a, b) => ({ a, b }));` : zustand ? `// ${r.component}
|
|
1694
|
+
import { useShallow } from 'zustand/react/shallow';
|
|
1695
|
+
const { a, b } = useStore(useShallow((s) => ({ a: s.a, b: s.b })));
|
|
1696
|
+
// or select a primitive: const a = useStore((s) => s.a);` : `// ${r.component}
|
|
1697
|
+
// getSnapshot must return the same reference while the data is unchanged
|
|
1698
|
+
const snapshot = useSyncExternalStore(subscribe, store.getSnapshot /* cached */);`
|
|
1699
|
+
});
|
|
1275
1700
|
} else {
|
|
1276
|
-
out.push({
|
|
1701
|
+
out.push({
|
|
1702
|
+
kind: "bailout",
|
|
1703
|
+
owner: r.component,
|
|
1704
|
+
target: r.component,
|
|
1705
|
+
prop: c.path,
|
|
1706
|
+
label: `bail out before setting ${c.path} in <${r.component}>`,
|
|
1707
|
+
detail: `${c.path} was set to a value deep-equal to the current one (new reference).`,
|
|
1708
|
+
snippet: `// ${r.component}
|
|
1709
|
+
setState((prev) => (deepEqual(prev, next) ? prev : next));`
|
|
1710
|
+
});
|
|
1277
1711
|
}
|
|
1278
1712
|
}
|
|
1279
1713
|
return out;
|
|
@@ -1287,11 +1721,12 @@ function rankFixes(reports) {
|
|
|
1287
1721
|
const key = fixKey(f);
|
|
1288
1722
|
let agg = byKey.get(key);
|
|
1289
1723
|
if (!agg) {
|
|
1290
|
-
agg = { ...f, key, count: 0, components: {} };
|
|
1724
|
+
agg = { ...f, key, count: 0, components: {}, reports: [] };
|
|
1291
1725
|
byKey.set(key, agg);
|
|
1292
1726
|
}
|
|
1293
1727
|
agg.count++;
|
|
1294
1728
|
agg.components[r.component] = (agg.components[r.component] || 0) + 1;
|
|
1729
|
+
if (agg.reports.length < MAX_FIX_REPORTS) agg.reports.push(r);
|
|
1295
1730
|
}
|
|
1296
1731
|
}
|
|
1297
1732
|
return [...byKey.values()].sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
|
|
@@ -1301,6 +1736,8 @@ function formatFixes(reports, limit = 10) {
|
|
|
1301
1736
|
if (!ranked.length) return "No avoidable re-renders.";
|
|
1302
1737
|
const lines = ranked.slice(0, limit).map((f, i) => `${String(i + 1).padStart(2)}. ${f.label} (removes ${f.count}: ${Object.entries(f.components).map(([c, n]) => `<${c}>${n > 1 ? " x" + n : ""}`).join(", ")})`);
|
|
1303
1738
|
if (ranked.length > limit) lines.push(` \u2026 ${ranked.length - limit} more`);
|
|
1739
|
+
const roots = formatRootCauses(reports, 5);
|
|
1740
|
+
if (roots) lines.push("", "Root causes:", roots);
|
|
1304
1741
|
return lines.join("\n");
|
|
1305
1742
|
}
|
|
1306
1743
|
|
|
@@ -1357,6 +1794,8 @@ function compareSummaries(before, after) {
|
|
|
1357
1794
|
regressions: rows.filter((r) => r.delta > 0)
|
|
1358
1795
|
};
|
|
1359
1796
|
}
|
|
1797
|
+
var summarizeSession = (session, reports) => summarizeReports(reports, session);
|
|
1798
|
+
var compareSessions = compareSummaries;
|
|
1360
1799
|
function formatComparison(c) {
|
|
1361
1800
|
const w = Math.max(14, ...c.rows.map((r) => r.component.length + 2));
|
|
1362
1801
|
const line = (label, b, a, d) => `${label.padEnd(w)} ${String(b).padStart(8)} ${String(a).padStart(8)} ${d.padStart(8)}`;
|
|
@@ -1555,7 +1994,7 @@ function flash(target, duration = 500) {
|
|
|
1555
1994
|
}
|
|
1556
1995
|
|
|
1557
1996
|
// src/version.ts
|
|
1558
|
-
var VERSION = "0.
|
|
1997
|
+
var VERSION = "0.7.0" ;
|
|
1559
1998
|
|
|
1560
1999
|
// src/devtools.ts
|
|
1561
2000
|
var DEVTOOLS_MARKER = "__rerenderLens";
|
|
@@ -1681,6 +2120,18 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1681
2120
|
target.postMessage(msg, "*");
|
|
1682
2121
|
};
|
|
1683
2122
|
const source = options.source ?? "page";
|
|
2123
|
+
const trackingSummary = () => {
|
|
2124
|
+
const s = getState();
|
|
2125
|
+
const o = s.options;
|
|
2126
|
+
return {
|
|
2127
|
+
mode: o.trackAllComponents ? "all" : o.trackAllMemoized ? "memoized" : "marked",
|
|
2128
|
+
include: (o.include ?? []).map(matcherToString).filter((x) => x !== null),
|
|
2129
|
+
exclude: (o.exclude ?? []).map(matcherToString).filter((x) => x !== null),
|
|
2130
|
+
renderedCount: s.seen.size,
|
|
2131
|
+
trackedCount: s.seenTracked.size,
|
|
2132
|
+
overflow: s.seenOverflow
|
|
2133
|
+
};
|
|
2134
|
+
};
|
|
1684
2135
|
const info = () => ({
|
|
1685
2136
|
count: buffer.length,
|
|
1686
2137
|
library: VERSION,
|
|
@@ -1694,7 +2145,8 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1694
2145
|
scheduled: getState().scheduled,
|
|
1695
2146
|
commits: getState().commits,
|
|
1696
2147
|
overhead: { totalMs: getState().overheadMs, maxCommitMs: getState().maxCommitMs },
|
|
1697
|
-
truncated: getState().truncated
|
|
2148
|
+
truncated: getState().truncated,
|
|
2149
|
+
tracking: trackingSummary()
|
|
1698
2150
|
});
|
|
1699
2151
|
const bridge = {
|
|
1700
2152
|
replay: () => {
|
|
@@ -1740,6 +2192,10 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1740
2192
|
flashAvoidable: (on) => {
|
|
1741
2193
|
flashOn = !!on;
|
|
1742
2194
|
},
|
|
2195
|
+
explain: (target2) => {
|
|
2196
|
+
const comp = typeof target2 === "number" ? fiberById(target2) : nearestComponent(fiberForNode(target2));
|
|
2197
|
+
return comp ? explainTracking(fiberType(comp), getState().options) : null;
|
|
2198
|
+
},
|
|
1743
2199
|
inspect: (node) => {
|
|
1744
2200
|
const comp = nearestComponent(fiberForNode(node));
|
|
1745
2201
|
if (!comp) return null;
|
|
@@ -1754,6 +2210,7 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1754
2210
|
component: fiberName(comp),
|
|
1755
2211
|
instanceId: id,
|
|
1756
2212
|
tracked: shouldTrack(fiberType(comp), getState().options),
|
|
2213
|
+
tracking: explainTracking(fiberType(comp), getState().options),
|
|
1757
2214
|
path,
|
|
1758
2215
|
reports: id === null ? [] : buffer.filter((e) => e.instanceId === id).slice(-20).map((e) => e.payload)
|
|
1759
2216
|
};
|
|
@@ -1788,6 +2245,9 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1788
2245
|
bridge.flashAvoidable(!!data.arg);
|
|
1789
2246
|
reply.result = true;
|
|
1790
2247
|
break;
|
|
2248
|
+
case "explain":
|
|
2249
|
+
reply.result = bridge.explain(data.arg);
|
|
2250
|
+
break;
|
|
1791
2251
|
default:
|
|
1792
2252
|
reply.error = `unknown command ${String(data.cmd)}`;
|
|
1793
2253
|
}
|
|
@@ -1796,6 +2256,7 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1796
2256
|
}
|
|
1797
2257
|
return reply;
|
|
1798
2258
|
};
|
|
2259
|
+
const isRelayGreeting = (data) => !!data && typeof data === "object" && data.__rerenderLensRelay === true && typeof data.app === "string";
|
|
1799
2260
|
const isCommand = (data) => !!data && typeof data === "object" && data.__rerenderLensCmd === true && typeof data.id === "string";
|
|
1800
2261
|
const envelope = (type, payload) => ({ [DEVTOOLS_MARKER]: true, version: PROTOCOL_VERSION, type, payload });
|
|
1801
2262
|
const sinks = [];
|
|
@@ -1818,14 +2279,20 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1818
2279
|
const relayUrl = options.relay ?? (typeof window !== "undefined" ? window.__RERENDER_LENS_RELAY__ : void 0);
|
|
1819
2280
|
const ES = options.eventSource ?? (typeof EventSource === "function" ? EventSource : null);
|
|
1820
2281
|
if (relayUrl && ES && typeof fetch === "function") {
|
|
1821
|
-
const
|
|
2282
|
+
const [rawBase, relayQuery = ""] = relayUrl.split("?");
|
|
2283
|
+
const base = (rawBase ?? "").replace(/\/$/, "");
|
|
2284
|
+
const relayToken = new URLSearchParams(relayQuery).get("token");
|
|
2285
|
+
const auth = relayToken ? `token=${encodeURIComponent(relayToken)}` : "";
|
|
1822
2286
|
let queue = [];
|
|
1823
2287
|
let scheduled = false;
|
|
2288
|
+
let appId = null;
|
|
1824
2289
|
const flush = () => {
|
|
1825
2290
|
scheduled = false;
|
|
1826
2291
|
const batch = queue;
|
|
1827
2292
|
queue = [];
|
|
1828
|
-
|
|
2293
|
+
const params = [appId ? `app=${encodeURIComponent(appId)}` : "", auth].filter(Boolean).join("&");
|
|
2294
|
+
const to = `${base}/message${params ? `?${params}` : ""}`;
|
|
2295
|
+
fetch(to, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(batch), keepalive: true }).catch(() => {
|
|
1829
2296
|
});
|
|
1830
2297
|
};
|
|
1831
2298
|
const relaySend = (message) => {
|
|
@@ -1836,7 +2303,8 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1836
2303
|
}
|
|
1837
2304
|
};
|
|
1838
2305
|
try {
|
|
1839
|
-
const
|
|
2306
|
+
const label = typeof location !== "undefined" ? `${location.host}${location.pathname}`.slice(0, 120) : "";
|
|
2307
|
+
const stream = new ES(`${base}/events?role=app${label ? `&label=${encodeURIComponent(label)}` : ""}${auth ? `&${auth}` : ""}`);
|
|
1840
2308
|
stream.onmessage = (event) => {
|
|
1841
2309
|
let parsed;
|
|
1842
2310
|
try {
|
|
@@ -1844,7 +2312,10 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1844
2312
|
} catch {
|
|
1845
2313
|
return;
|
|
1846
2314
|
}
|
|
1847
|
-
for (const m of Array.isArray(parsed) ? parsed : [parsed])
|
|
2315
|
+
for (const m of Array.isArray(parsed) ? parsed : [parsed]) {
|
|
2316
|
+
if (isRelayGreeting(m)) appId = m.app;
|
|
2317
|
+
else if (isCommand(m)) relaySend(runCommand(m));
|
|
2318
|
+
}
|
|
1848
2319
|
};
|
|
1849
2320
|
stream.onerror = () => {
|
|
1850
2321
|
};
|
|
@@ -1887,6 +2358,6 @@ function createDevtoolsNotifier(options = {}) {
|
|
|
1887
2358
|
};
|
|
1888
2359
|
}
|
|
1889
2360
|
|
|
1890
|
-
export { DEFAULT_CHANNEL, DEVTOOLS_MARKER, MARKER, PROTOCOL_VERSION, VERSION, assertWithinBudget, avoidableCounts, buildReport, checkBudget, classify, combineNotifiers, compareSummaries, configure, createCollector, createDevtoolsNotifier, customHooksFromStack, deepEqual, deserializeOptions, diffRecords, disable, ensureDevtoolsHook, fixKey, fixesFor, formatComparison, formatFixes, getDisplayName, getRenderers, hookLabel, init, isEnabled, isProductionReact, parseExport, printReport, rankFixes, resolveHookNames, serialize, serializeOptions, shouldTrack, storeAdvice, summarize, summarizeReports, toBudget, track, useWhyRerender };
|
|
2361
|
+
export { AVOIDABLE_KINDS, DEFAULT_CHANNEL, DEVTOOLS_MARKER, KIND_LABEL, MARKER, PROTOCOL_VERSION, VERSION, analyzeCommit, assertWithinBudget, avoidableCounts, buildReport, cascadeTree, checkBudget, classify, combineNotifiers, compareSessions, compareSummaries, configure, contextAttribution, createCollector, createDevtoolsNotifier, customHooksFromStack, deepEqual, deserializeOptions, diffLeaves, diffRecords, disable, ensureDevtoolsHook, explainTracking, firstDifferentPath, fixKey, fixesFor, formatComparison, formatFixes, formatRootCauses, getDisplayName, getRenderers, groupByCommit, hookLabel, indexByComponent, init, isEnabled, isProductionReact, parseExport, printReport, rankFixes, rankRootCauses, resolveHookNames, rootCauseOf, rootCauseSummary, serialize, serializeOptions, shortValue, shouldTrack, storeAdvice, summarize, summarizeReports, summarizeSession, toBudget, track, useWhyRerender };
|
|
1891
2362
|
//# sourceMappingURL=index.js.map
|
|
1892
2363
|
//# sourceMappingURL=index.js.map
|