@react-perfscope/react 0.2.0 → 0.4.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/dist/index.cjs +26 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +26 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -236,6 +236,14 @@ function classifyRenderReason(fiber) {
|
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
// src/render-collector.ts
|
|
239
|
+
function cascadeRoot(members) {
|
|
240
|
+
let shallowest = members[0];
|
|
241
|
+
for (const m of members) {
|
|
242
|
+
if (m.reason === "state") return m;
|
|
243
|
+
if (m.depth < shallowest.depth) shallowest = m;
|
|
244
|
+
}
|
|
245
|
+
return members.find((m) => m.reason === "mount") ?? shallowest;
|
|
246
|
+
}
|
|
239
247
|
function createRenderCollector() {
|
|
240
248
|
let active = false;
|
|
241
249
|
let emit = () => {
|
|
@@ -246,6 +254,7 @@ function createRenderCollector() {
|
|
|
246
254
|
if (!active) return;
|
|
247
255
|
const at = performance.now();
|
|
248
256
|
const id = commitId++;
|
|
257
|
+
const members = [];
|
|
249
258
|
walkChangedFibers(
|
|
250
259
|
root.current,
|
|
251
260
|
(fiber, depth) => {
|
|
@@ -255,7 +264,7 @@ function createRenderCollector() {
|
|
|
255
264
|
if (!name) return;
|
|
256
265
|
const duration = typeof fiber.actualDuration === "number" ? fiber.actualDuration : 0;
|
|
257
266
|
const { reason, changedProps } = classifyRenderReason(fiber);
|
|
258
|
-
|
|
267
|
+
members.push({
|
|
259
268
|
kind: "render",
|
|
260
269
|
at,
|
|
261
270
|
component: name,
|
|
@@ -271,6 +280,22 @@ function createRenderCollector() {
|
|
|
271
280
|
// as a phantom render on every unrelated commit.
|
|
272
281
|
{ descend: subtreeMightHaveRendered }
|
|
273
282
|
);
|
|
283
|
+
if (members.length === 0) return;
|
|
284
|
+
const root2 = cascadeRoot(members);
|
|
285
|
+
let total = 0;
|
|
286
|
+
for (const m of members) total += m.duration;
|
|
287
|
+
emit({
|
|
288
|
+
kind: "render",
|
|
289
|
+
at,
|
|
290
|
+
component: root2.component,
|
|
291
|
+
reason: root2.reason,
|
|
292
|
+
duration: total,
|
|
293
|
+
commitId: id,
|
|
294
|
+
depth: root2.depth,
|
|
295
|
+
...root2.changedProps ? { changedProps: root2.changedProps } : {},
|
|
296
|
+
members,
|
|
297
|
+
count: members.length
|
|
298
|
+
});
|
|
274
299
|
}
|
|
275
300
|
return {
|
|
276
301
|
kind: "render",
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/devtools-hook.ts","../src/fiber-walker.ts","../src/attribution.ts","../src/render-reason.ts","../src/render-collector.ts"],"sourcesContent":["export * from './types'\nexport { installDevToolsHook, uninstallDevToolsHook } from './devtools-hook'\nexport { fiberComponentName, walkChangedFibers } from './fiber-walker'\nexport { resolveComponentFromElement } from './attribution'\nexport { createRenderCollector } from './render-collector'\n","import type { MinimalFiber, ReactDevToolsHook } from './types'\n\ntype CommitListener = (root: { current: MinimalFiber }, rendererId: number) => void\n\nconst HOOK_KEY = '__REACT_DEVTOOLS_GLOBAL_HOOK__'\n\ninterface GlobalWithHook {\n [HOOK_KEY]?: ReactDevToolsHook\n}\n\nconst listeners = new Set<CommitListener>()\nlet ourHook: ReactDevToolsHook | null = null\nlet chainedOriginal: ReactDevToolsHook['onCommitFiberRoot'] | null = null\n\nfunction ensureHookInstalled(): void {\n const g = globalThis as GlobalWithHook\n // Already ours and still installed — nothing to do.\n if (ourHook && g[HOOK_KEY] === ourHook) return\n\n const existing = g[HOOK_KEY]\n chainedOriginal =\n existing && existing !== ourHook && typeof existing.onCommitFiberRoot === 'function'\n ? existing.onCommitFiberRoot\n : null\n\n const hook: ReactDevToolsHook = existing && existing !== ourHook ? existing : {}\n // React's injectInternals() checks for supportsFiber and calls inject() to\n // get a renderer ID. Without these, React stores injectedHook = null and\n // never calls onCommitFiberRoot on subsequent commits.\n if (!hook.supportsFiber) {\n hook.supportsFiber = true\n }\n if (typeof hook.inject !== 'function') {\n let _nextRendererId = 1\n hook.inject = () => _nextRendererId++\n }\n // Additional fields that React Refresh (@vitejs/plugin-react) accesses on\n // the hook. Without these it throws \"Cannot read properties of undefined\n // (reading 'forEach')\" at injectIntoGlobalHook. We default each to a safe\n // empty/noop value; if the real React DevTools extension installs later\n // it will overwrite these.\n if (!hook.renderers) {\n ;(hook as Record<string, unknown>).renderers = new Map()\n }\n if (typeof (hook as Record<string, unknown>)['on'] !== 'function') {\n ;(hook as Record<string, unknown>)['on'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['off'] !== 'function') {\n ;(hook as Record<string, unknown>)['off'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['emit'] !== 'function') {\n ;(hook as Record<string, unknown>)['emit'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['sub'] !== 'function') {\n ;(hook as Record<string, unknown>)['sub'] = () => () => {}\n }\n if (typeof (hook as Record<string, unknown>)['checkDCE'] !== 'function') {\n ;(hook as Record<string, unknown>)['checkDCE'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['onCommitFiberUnmount'] !== 'function') {\n ;(hook as Record<string, unknown>)['onCommitFiberUnmount'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['onPostCommitFiberRoot'] !== 'function') {\n ;(hook as Record<string, unknown>)['onPostCommitFiberRoot'] = () => {}\n }\n hook.onCommitFiberRoot = (rendererId, root, priorityLevel) => {\n if (chainedOriginal) {\n try {\n chainedOriginal(rendererId, root, priorityLevel)\n } catch (err) {\n console.warn('[react-perfscope] chained DevTools hook threw:', err)\n }\n }\n for (const cb of listeners) {\n try {\n cb(root, rendererId)\n } catch (err) {\n console.warn('[react-perfscope] commit listener threw:', err)\n }\n }\n }\n g[HOOK_KEY] = hook\n ourHook = hook\n}\n\nexport function installDevToolsHook(listener: CommitListener): () => void {\n ensureHookInstalled()\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\n/**\n * Clears all listeners and forgets our installed hook reference. Used by\n * tests to fully reset module state. Does NOT remove the hook from\n * globalThis — callers that want a fully clean slate must also\n * `delete globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__`.\n */\nexport function uninstallDevToolsHook(): void {\n listeners.clear()\n ourHook = null\n chainedOriginal = null\n}\n","import type { MinimalFiber } from './types'\n\nconst MEMO_TYPE = Symbol.for('react.memo')\nconst FORWARD_REF_TYPE = Symbol.for('react.forward_ref')\n\ntype WrapperType = {\n $$typeof: symbol\n type?: unknown\n render?: unknown\n}\n\nfunction namedFunctionName(value: unknown): string | null {\n if (typeof value !== 'function') return null\n const fn = value as { displayName?: string; name?: string }\n if (typeof fn.displayName === 'string' && fn.displayName.length > 0) return fn.displayName\n if (typeof fn.name === 'string' && fn.name.length > 0) return fn.name\n return null\n}\n\n/**\n * Return the component name for a fiber.\n *\n * Host components (DOM tags) return their tag string. Function and class\n * components return their displayName or name. Memo and forwardRef wrappers\n * are unwrapped to their inner component. Unknown shapes return null.\n */\nexport function fiberComponentName(fiber: MinimalFiber | null): string | null {\n if (!fiber) return null\n const type = fiber.type\n if (typeof type === 'string') return type\n if (typeof type === 'function') return namedFunctionName(type)\n if (type && typeof type === 'object') {\n const wrapper = type as WrapperType\n if (wrapper.$$typeof === MEMO_TYPE) {\n return namedFunctionName(wrapper.type)\n }\n if (wrapper.$$typeof === FORWARD_REF_TYPE) {\n return namedFunctionName(wrapper.render)\n }\n }\n return null\n}\n\ninterface WalkOptions {\n /**\n * Maximum number of fibers to visit. Prevents runaway traversal on very\n * deep trees. Default 10_000.\n */\n stopAt?: number\n /**\n * Predicate deciding whether to descend into a fiber's children. Returning\n * false prunes the entire subtree (the fiber itself is still visited). Used\n * to skip subtrees that did no work this commit. Defaults to always descend.\n */\n descend?: (fiber: MinimalFiber) => boolean\n}\n\n/**\n * Walk a fiber subtree depth-first, invoking `visit` for every fiber with its\n * depth relative to `root` (root is depth 0). Returns early once `stopAt`\n * fibers have been visited. When `descend` returns false for a fiber, its\n * children are skipped entirely.\n */\nexport function walkChangedFibers(\n root: MinimalFiber,\n visit: (fiber: MinimalFiber, depth: number) => void,\n opts: WalkOptions = {}\n): void {\n const max = opts.stopAt ?? 10_000\n const descend = opts.descend ?? (() => true)\n let count = 0\n let depth = 0\n let node: MinimalFiber | null = root\n while (node) {\n visit(node, depth)\n count++\n if (count >= max) return\n if (node.child && descend(node)) {\n node = node.child\n depth++\n continue\n }\n while (node && !node.sibling) {\n if (node === root) return\n node = node.return\n depth--\n }\n if (!node || node === root) return\n node = node.sibling\n }\n}\n","import type { MinimalFiber } from './types'\nimport { fiberComponentName } from './fiber-walker'\n\nconst FIBER_KEY_PATTERNS = ['__reactFiber$', '__reactInternalInstance$']\n\nfunction findFiberOnElement(el: HTMLElement): MinimalFiber | null {\n for (const key of Object.keys(el)) {\n for (const pattern of FIBER_KEY_PATTERNS) {\n if (key.startsWith(pattern)) {\n return (el as HTMLElement & Record<string, MinimalFiber>)[key] ?? null\n }\n }\n }\n return null\n}\n\n/**\n * Walk up from the fiber attached to `el` until we find one whose `type` is\n * a function or class component (not a host tag string). Returns that\n * component's display name. If no component is found, returns the host\n * tag name of the starting fiber. If no fiber is attached, returns null.\n */\nexport function resolveComponentFromElement(el: HTMLElement): string | null {\n const start = findFiberOnElement(el)\n if (!start) return null\n let node: MinimalFiber | null = start\n while (node) {\n if (typeof node.type === 'function' || (node.type && typeof node.type === 'object')) {\n const name = fiberComponentName(node)\n if (name) return name\n }\n node = node.return\n }\n // Nothing but host fibers above — return the host tag name of the starting fiber.\n return fiberComponentName(start)\n}\n","import type { MinimalFiber } from './types'\n\n/** React's `PerformedWork` flag — set on a fiber whose render actually ran. */\nexport const PERFORMED_WORK = 0b1\n\nexport type RenderReason = 'mount' | 'state' | 'props' | 'parent'\n\nexport interface RenderReasonResult {\n reason: RenderReason\n changedProps?: string[]\n}\n\nexport function didPerformWork(fiber: MinimalFiber | null): boolean {\n if (!fiber || typeof fiber.flags !== 'number') return false\n return (fiber.flags & PERFORMED_WORK) !== 0\n}\n\n/**\n * Should the walk descend into this fiber's children when hunting for renders?\n *\n * React bubbles the `PerformedWork` bit of every descendant into a fiber's\n * `subtreeFlags`, and (unlike `flags`) resets it whenever the reconciler\n * re-clones the fiber. So a numeric `subtreeFlags` without the bit reliably\n * means \"no descendant rendered this commit\" — we can stop here and avoid\n * stale leaves whose own `flags` never got cleared.\n *\n * When `subtreeFlags` is absent (React < 18) we cannot prune safely, so we\n * descend and fall back to the full-tree walk.\n */\nexport function subtreeMightHaveRendered(fiber: MinimalFiber): boolean {\n if (typeof fiber.subtreeFlags !== 'number') return true\n return (fiber.subtreeFlags & PERFORMED_WORK) !== 0\n}\n\n/**\n * Walk up `.return` to the nearest *component* fiber, skipping host (DOM)\n * fibers. A component's immediate `.return` is usually a host fiber (the\n * `<div>` it's nested in), not its parent component — so to ask \"did my\n * parent component render?\" we have to climb past the host nodes first.\n */\nexport function nearestComponentAncestor(fiber: MinimalFiber): MinimalFiber | null {\n let p = fiber.return\n while (p) {\n if (typeof p.type === 'function') return p\n p = p.return\n }\n return null\n}\n\nfunction asObject(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' ? (value as Record<string, unknown>) : {}\n}\n\n/** Shallow diff of two prop bags. Returns the keys whose values differ. */\nexport function changedPropKeys(prev: unknown, next: unknown): string[] {\n if (prev === next) return []\n const p = asObject(prev)\n const n = asObject(next)\n const keys = new Set([...Object.keys(p), ...Object.keys(n)])\n const changed: string[] = []\n for (const k of keys) {\n if (p[k] !== n[k]) changed.push(k)\n }\n return changed\n}\n\n/**\n * Classify WHY a fiber that performed work this commit rendered.\n *\n * - `mount` — first render (no previous fiber).\n * - `props` — at least one prop changed since the previous render.\n * - `state` — props are identical AND the parent did not render, so the\n * update originated here (own state/force update). This is the\n * root of a render cascade.\n * - `parent` — props are identical but the parent rendered, so this only\n * re-rendered because of its parent. This is the avoidable\n * \"wrap me in memo\" case the developer is hunting for.\n *\n * Assumes the caller only invokes this for fibers that actually performed\n * work (see `didPerformWork`).\n */\nexport function classifyRenderReason(fiber: MinimalFiber): RenderReasonResult {\n if (!fiber.alternate) return { reason: 'mount' }\n const changed = changedPropKeys(fiber.alternate.memoizedProps, fiber.memoizedProps)\n if (changed.length > 0) return { reason: 'props', changedProps: changed }\n if (didPerformWork(nearestComponentAncestor(fiber))) return { reason: 'parent' }\n return { reason: 'state' }\n}\n","import type { Collector, Signal } from '@react-perfscope/core'\nimport { installDevToolsHook } from './devtools-hook'\nimport { fiberComponentName, walkChangedFibers } from './fiber-walker'\nimport { classifyRenderReason, didPerformWork, subtreeMightHaveRendered } from './render-reason'\nimport type { MinimalFiber } from './types'\n\nexport function createRenderCollector(): Collector {\n let active = false\n let emit: (signal: Signal) => void = () => {}\n let unsubscribe: (() => void) | null = null\n let commitId = 0\n\n function onCommit(root: { current: MinimalFiber }) {\n if (!active) return\n const at = performance.now()\n const id = commitId++\n walkChangedFibers(\n root.current,\n (fiber, depth) => {\n // Skip host (DOM) fibers — we only want function/class components in\n // render reports.\n if (typeof fiber.type === 'string') return\n // Only report fibers that actually re-ran their render this commit.\n // A bailed-out fiber (e.g. memo with equal props) keeps its place in\n // the tree but did no work, so reporting it would be noise.\n if (!didPerformWork(fiber)) return\n const name = fiberComponentName(fiber)\n if (!name) return\n const duration = typeof fiber.actualDuration === 'number' ? fiber.actualDuration : 0\n const { reason, changedProps } = classifyRenderReason(fiber)\n emit({\n kind: 'render',\n at,\n component: name,\n reason,\n duration,\n commitId: id,\n depth,\n ...(changedProps ? { changedProps } : {}),\n })\n },\n // Prune subtrees that did no work this commit. Without this, a leaf that\n // mounted long ago keeps its stale PerformedWork flag and gets reported\n // as a phantom render on every unrelated commit.\n { descend: subtreeMightHaveRendered }\n )\n }\n\n return {\n kind: 'render',\n activate(emitFn) {\n emit = emitFn\n active = true\n if (unsubscribe) return\n unsubscribe = installDevToolsHook(onCommit)\n },\n deactivate() {\n active = false\n // Keep the global hook listener attached — installDevToolsHook is\n // idempotent and removing it on every deactivate would cost us the\n // ability to re-attach cleanly under the test reset hooks. The\n // `active` flag gates emission.\n },\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/devtools-hook.ts","../src/fiber-walker.ts","../src/attribution.ts","../src/render-reason.ts","../src/render-collector.ts"],"sourcesContent":["export * from './types'\nexport { installDevToolsHook, uninstallDevToolsHook } from './devtools-hook'\nexport { fiberComponentName, walkChangedFibers } from './fiber-walker'\nexport { resolveComponentFromElement } from './attribution'\nexport { createRenderCollector } from './render-collector'\n","import type { MinimalFiber, ReactDevToolsHook } from './types'\n\ntype CommitListener = (root: { current: MinimalFiber }, rendererId: number) => void\n\nconst HOOK_KEY = '__REACT_DEVTOOLS_GLOBAL_HOOK__'\n\ninterface GlobalWithHook {\n [HOOK_KEY]?: ReactDevToolsHook\n}\n\nconst listeners = new Set<CommitListener>()\nlet ourHook: ReactDevToolsHook | null = null\nlet chainedOriginal: ReactDevToolsHook['onCommitFiberRoot'] | null = null\n\nfunction ensureHookInstalled(): void {\n const g = globalThis as GlobalWithHook\n // Already ours and still installed — nothing to do.\n if (ourHook && g[HOOK_KEY] === ourHook) return\n\n const existing = g[HOOK_KEY]\n chainedOriginal =\n existing && existing !== ourHook && typeof existing.onCommitFiberRoot === 'function'\n ? existing.onCommitFiberRoot\n : null\n\n const hook: ReactDevToolsHook = existing && existing !== ourHook ? existing : {}\n // React's injectInternals() checks for supportsFiber and calls inject() to\n // get a renderer ID. Without these, React stores injectedHook = null and\n // never calls onCommitFiberRoot on subsequent commits.\n if (!hook.supportsFiber) {\n hook.supportsFiber = true\n }\n if (typeof hook.inject !== 'function') {\n let _nextRendererId = 1\n hook.inject = () => _nextRendererId++\n }\n // Additional fields that React Refresh (@vitejs/plugin-react) accesses on\n // the hook. Without these it throws \"Cannot read properties of undefined\n // (reading 'forEach')\" at injectIntoGlobalHook. We default each to a safe\n // empty/noop value; if the real React DevTools extension installs later\n // it will overwrite these.\n if (!hook.renderers) {\n ;(hook as Record<string, unknown>).renderers = new Map()\n }\n if (typeof (hook as Record<string, unknown>)['on'] !== 'function') {\n ;(hook as Record<string, unknown>)['on'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['off'] !== 'function') {\n ;(hook as Record<string, unknown>)['off'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['emit'] !== 'function') {\n ;(hook as Record<string, unknown>)['emit'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['sub'] !== 'function') {\n ;(hook as Record<string, unknown>)['sub'] = () => () => {}\n }\n if (typeof (hook as Record<string, unknown>)['checkDCE'] !== 'function') {\n ;(hook as Record<string, unknown>)['checkDCE'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['onCommitFiberUnmount'] !== 'function') {\n ;(hook as Record<string, unknown>)['onCommitFiberUnmount'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['onPostCommitFiberRoot'] !== 'function') {\n ;(hook as Record<string, unknown>)['onPostCommitFiberRoot'] = () => {}\n }\n hook.onCommitFiberRoot = (rendererId, root, priorityLevel) => {\n if (chainedOriginal) {\n try {\n chainedOriginal(rendererId, root, priorityLevel)\n } catch (err) {\n console.warn('[react-perfscope] chained DevTools hook threw:', err)\n }\n }\n for (const cb of listeners) {\n try {\n cb(root, rendererId)\n } catch (err) {\n console.warn('[react-perfscope] commit listener threw:', err)\n }\n }\n }\n g[HOOK_KEY] = hook\n ourHook = hook\n}\n\nexport function installDevToolsHook(listener: CommitListener): () => void {\n ensureHookInstalled()\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\n/**\n * Clears all listeners and forgets our installed hook reference. Used by\n * tests to fully reset module state. Does NOT remove the hook from\n * globalThis — callers that want a fully clean slate must also\n * `delete globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__`.\n */\nexport function uninstallDevToolsHook(): void {\n listeners.clear()\n ourHook = null\n chainedOriginal = null\n}\n","import type { MinimalFiber } from './types'\n\nconst MEMO_TYPE = Symbol.for('react.memo')\nconst FORWARD_REF_TYPE = Symbol.for('react.forward_ref')\n\ntype WrapperType = {\n $$typeof: symbol\n type?: unknown\n render?: unknown\n}\n\nfunction namedFunctionName(value: unknown): string | null {\n if (typeof value !== 'function') return null\n const fn = value as { displayName?: string; name?: string }\n if (typeof fn.displayName === 'string' && fn.displayName.length > 0) return fn.displayName\n if (typeof fn.name === 'string' && fn.name.length > 0) return fn.name\n return null\n}\n\n/**\n * Return the component name for a fiber.\n *\n * Host components (DOM tags) return their tag string. Function and class\n * components return their displayName or name. Memo and forwardRef wrappers\n * are unwrapped to their inner component. Unknown shapes return null.\n */\nexport function fiberComponentName(fiber: MinimalFiber | null): string | null {\n if (!fiber) return null\n const type = fiber.type\n if (typeof type === 'string') return type\n if (typeof type === 'function') return namedFunctionName(type)\n if (type && typeof type === 'object') {\n const wrapper = type as WrapperType\n if (wrapper.$$typeof === MEMO_TYPE) {\n return namedFunctionName(wrapper.type)\n }\n if (wrapper.$$typeof === FORWARD_REF_TYPE) {\n return namedFunctionName(wrapper.render)\n }\n }\n return null\n}\n\ninterface WalkOptions {\n /**\n * Maximum number of fibers to visit. Prevents runaway traversal on very\n * deep trees. Default 10_000.\n */\n stopAt?: number\n /**\n * Predicate deciding whether to descend into a fiber's children. Returning\n * false prunes the entire subtree (the fiber itself is still visited). Used\n * to skip subtrees that did no work this commit. Defaults to always descend.\n */\n descend?: (fiber: MinimalFiber) => boolean\n}\n\n/**\n * Walk a fiber subtree depth-first, invoking `visit` for every fiber with its\n * depth relative to `root` (root is depth 0). Returns early once `stopAt`\n * fibers have been visited. When `descend` returns false for a fiber, its\n * children are skipped entirely.\n */\nexport function walkChangedFibers(\n root: MinimalFiber,\n visit: (fiber: MinimalFiber, depth: number) => void,\n opts: WalkOptions = {}\n): void {\n const max = opts.stopAt ?? 10_000\n const descend = opts.descend ?? (() => true)\n let count = 0\n let depth = 0\n let node: MinimalFiber | null = root\n while (node) {\n visit(node, depth)\n count++\n if (count >= max) return\n if (node.child && descend(node)) {\n node = node.child\n depth++\n continue\n }\n while (node && !node.sibling) {\n if (node === root) return\n node = node.return\n depth--\n }\n if (!node || node === root) return\n node = node.sibling\n }\n}\n","import type { MinimalFiber } from './types'\nimport { fiberComponentName } from './fiber-walker'\n\nconst FIBER_KEY_PATTERNS = ['__reactFiber$', '__reactInternalInstance$']\n\nfunction findFiberOnElement(el: HTMLElement): MinimalFiber | null {\n for (const key of Object.keys(el)) {\n for (const pattern of FIBER_KEY_PATTERNS) {\n if (key.startsWith(pattern)) {\n return (el as HTMLElement & Record<string, MinimalFiber>)[key] ?? null\n }\n }\n }\n return null\n}\n\n/**\n * Walk up from the fiber attached to `el` until we find one whose `type` is\n * a function or class component (not a host tag string). Returns that\n * component's display name. If no component is found, returns the host\n * tag name of the starting fiber. If no fiber is attached, returns null.\n */\nexport function resolveComponentFromElement(el: HTMLElement): string | null {\n const start = findFiberOnElement(el)\n if (!start) return null\n let node: MinimalFiber | null = start\n while (node) {\n if (typeof node.type === 'function' || (node.type && typeof node.type === 'object')) {\n const name = fiberComponentName(node)\n if (name) return name\n }\n node = node.return\n }\n // Nothing but host fibers above — return the host tag name of the starting fiber.\n return fiberComponentName(start)\n}\n","import type { MinimalFiber } from './types'\n\n/** React's `PerformedWork` flag — set on a fiber whose render actually ran. */\nexport const PERFORMED_WORK = 0b1\n\nexport type RenderReason = 'mount' | 'state' | 'props' | 'parent'\n\nexport interface RenderReasonResult {\n reason: RenderReason\n changedProps?: string[]\n}\n\nexport function didPerformWork(fiber: MinimalFiber | null): boolean {\n if (!fiber || typeof fiber.flags !== 'number') return false\n return (fiber.flags & PERFORMED_WORK) !== 0\n}\n\n/**\n * Should the walk descend into this fiber's children when hunting for renders?\n *\n * React bubbles the `PerformedWork` bit of every descendant into a fiber's\n * `subtreeFlags`, and (unlike `flags`) resets it whenever the reconciler\n * re-clones the fiber. So a numeric `subtreeFlags` without the bit reliably\n * means \"no descendant rendered this commit\" — we can stop here and avoid\n * stale leaves whose own `flags` never got cleared.\n *\n * When `subtreeFlags` is absent (React < 18) we cannot prune safely, so we\n * descend and fall back to the full-tree walk.\n */\nexport function subtreeMightHaveRendered(fiber: MinimalFiber): boolean {\n if (typeof fiber.subtreeFlags !== 'number') return true\n return (fiber.subtreeFlags & PERFORMED_WORK) !== 0\n}\n\n/**\n * Walk up `.return` to the nearest *component* fiber, skipping host (DOM)\n * fibers. A component's immediate `.return` is usually a host fiber (the\n * `<div>` it's nested in), not its parent component — so to ask \"did my\n * parent component render?\" we have to climb past the host nodes first.\n */\nexport function nearestComponentAncestor(fiber: MinimalFiber): MinimalFiber | null {\n let p = fiber.return\n while (p) {\n if (typeof p.type === 'function') return p\n p = p.return\n }\n return null\n}\n\nfunction asObject(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' ? (value as Record<string, unknown>) : {}\n}\n\n/** Shallow diff of two prop bags. Returns the keys whose values differ. */\nexport function changedPropKeys(prev: unknown, next: unknown): string[] {\n if (prev === next) return []\n const p = asObject(prev)\n const n = asObject(next)\n const keys = new Set([...Object.keys(p), ...Object.keys(n)])\n const changed: string[] = []\n for (const k of keys) {\n if (p[k] !== n[k]) changed.push(k)\n }\n return changed\n}\n\n/**\n * Classify WHY a fiber that performed work this commit rendered.\n *\n * - `mount` — first render (no previous fiber).\n * - `props` — at least one prop changed since the previous render.\n * - `state` — props are identical AND the parent did not render, so the\n * update originated here (own state/force update). This is the\n * root of a render cascade.\n * - `parent` — props are identical but the parent rendered, so this only\n * re-rendered because of its parent. This is the avoidable\n * \"wrap me in memo\" case the developer is hunting for.\n *\n * Assumes the caller only invokes this for fibers that actually performed\n * work (see `didPerformWork`).\n */\nexport function classifyRenderReason(fiber: MinimalFiber): RenderReasonResult {\n if (!fiber.alternate) return { reason: 'mount' }\n const changed = changedPropKeys(fiber.alternate.memoizedProps, fiber.memoizedProps)\n if (changed.length > 0) return { reason: 'props', changedProps: changed }\n if (didPerformWork(nearestComponentAncestor(fiber))) return { reason: 'parent' }\n return { reason: 'state' }\n}\n","import type { Collector, RenderSignal, Signal } from '@react-perfscope/core'\nimport { installDevToolsHook } from './devtools-hook'\nimport { fiberComponentName, walkChangedFibers } from './fiber-walker'\nimport { classifyRenderReason, didPerformWork, subtreeMightHaveRendered } from './render-reason'\nimport type { MinimalFiber } from './types'\n\n/** The cascade root of a commit: the component whose own state/mount started\n * it. Falls back to the shallowest member. */\nfunction cascadeRoot(members: RenderSignal[]): RenderSignal {\n let shallowest = members[0]!\n for (const m of members) {\n if (m.reason === 'state') return m\n if (m.depth < shallowest.depth) shallowest = m\n }\n return members.find((m) => m.reason === 'mount') ?? shallowest\n}\n\nexport function createRenderCollector(): Collector {\n let active = false\n let emit: (signal: Signal) => void = () => {}\n let unsubscribe: (() => void) | null = null\n let commitId = 0\n\n function onCommit(root: { current: MinimalFiber }) {\n if (!active) return\n const at = performance.now()\n const id = commitId++\n const members: RenderSignal[] = []\n walkChangedFibers(\n root.current,\n (fiber, depth) => {\n // Skip host (DOM) fibers — we only want function/class components in\n // render reports.\n if (typeof fiber.type === 'string') return\n // Only report fibers that actually re-ran their render this commit.\n // A bailed-out fiber (e.g. memo with equal props) keeps its place in\n // the tree but did no work, so reporting it would be noise.\n if (!didPerformWork(fiber)) return\n const name = fiberComponentName(fiber)\n if (!name) return\n const duration = typeof fiber.actualDuration === 'number' ? fiber.actualDuration : 0\n const { reason, changedProps } = classifyRenderReason(fiber)\n members.push({\n kind: 'render',\n at,\n component: name,\n reason,\n duration,\n commitId: id,\n depth,\n ...(changedProps ? { changedProps } : {}),\n })\n },\n // Prune subtrees that did no work this commit. Without this, a leaf that\n // mounted long ago keeps its stale PerformedWork flag and gets reported\n // as a phantom render on every unrelated commit.\n { descend: subtreeMightHaveRendered }\n )\n if (members.length === 0) return\n // One coalesced signal per commit. The root describes the cascade; the\n // duration is the commit's total render time.\n const root2 = cascadeRoot(members)\n let total = 0\n for (const m of members) total += m.duration\n emit({\n kind: 'render',\n at,\n component: root2.component,\n reason: root2.reason,\n duration: total,\n commitId: id,\n depth: root2.depth,\n ...(root2.changedProps ? { changedProps: root2.changedProps } : {}),\n members,\n count: members.length,\n })\n }\n\n return {\n kind: 'render',\n activate(emitFn) {\n emit = emitFn\n active = true\n if (unsubscribe) return\n unsubscribe = installDevToolsHook(onCommit)\n },\n deactivate() {\n active = false\n // Keep the global hook listener attached — installDevToolsHook is\n // idempotent and removing it on every deactivate would cost us the\n // ability to re-attach cleanly under the test reset hooks. The\n // `active` flag gates emission.\n },\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -205,6 +205,14 @@ function classifyRenderReason(fiber) {
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
// src/render-collector.ts
|
|
208
|
+
function cascadeRoot(members) {
|
|
209
|
+
let shallowest = members[0];
|
|
210
|
+
for (const m of members) {
|
|
211
|
+
if (m.reason === "state") return m;
|
|
212
|
+
if (m.depth < shallowest.depth) shallowest = m;
|
|
213
|
+
}
|
|
214
|
+
return members.find((m) => m.reason === "mount") ?? shallowest;
|
|
215
|
+
}
|
|
208
216
|
function createRenderCollector() {
|
|
209
217
|
let active = false;
|
|
210
218
|
let emit = () => {
|
|
@@ -215,6 +223,7 @@ function createRenderCollector() {
|
|
|
215
223
|
if (!active) return;
|
|
216
224
|
const at = performance.now();
|
|
217
225
|
const id = commitId++;
|
|
226
|
+
const members = [];
|
|
218
227
|
walkChangedFibers(
|
|
219
228
|
root.current,
|
|
220
229
|
(fiber, depth) => {
|
|
@@ -224,7 +233,7 @@ function createRenderCollector() {
|
|
|
224
233
|
if (!name) return;
|
|
225
234
|
const duration = typeof fiber.actualDuration === "number" ? fiber.actualDuration : 0;
|
|
226
235
|
const { reason, changedProps } = classifyRenderReason(fiber);
|
|
227
|
-
|
|
236
|
+
members.push({
|
|
228
237
|
kind: "render",
|
|
229
238
|
at,
|
|
230
239
|
component: name,
|
|
@@ -240,6 +249,22 @@ function createRenderCollector() {
|
|
|
240
249
|
// as a phantom render on every unrelated commit.
|
|
241
250
|
{ descend: subtreeMightHaveRendered }
|
|
242
251
|
);
|
|
252
|
+
if (members.length === 0) return;
|
|
253
|
+
const root2 = cascadeRoot(members);
|
|
254
|
+
let total = 0;
|
|
255
|
+
for (const m of members) total += m.duration;
|
|
256
|
+
emit({
|
|
257
|
+
kind: "render",
|
|
258
|
+
at,
|
|
259
|
+
component: root2.component,
|
|
260
|
+
reason: root2.reason,
|
|
261
|
+
duration: total,
|
|
262
|
+
commitId: id,
|
|
263
|
+
depth: root2.depth,
|
|
264
|
+
...root2.changedProps ? { changedProps: root2.changedProps } : {},
|
|
265
|
+
members,
|
|
266
|
+
count: members.length
|
|
267
|
+
});
|
|
243
268
|
}
|
|
244
269
|
return {
|
|
245
270
|
kind: "render",
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/devtools-hook.ts","../src/fiber-walker.ts","../src/attribution.ts","../src/render-reason.ts","../src/render-collector.ts"],"sourcesContent":["import type { MinimalFiber, ReactDevToolsHook } from './types'\n\ntype CommitListener = (root: { current: MinimalFiber }, rendererId: number) => void\n\nconst HOOK_KEY = '__REACT_DEVTOOLS_GLOBAL_HOOK__'\n\ninterface GlobalWithHook {\n [HOOK_KEY]?: ReactDevToolsHook\n}\n\nconst listeners = new Set<CommitListener>()\nlet ourHook: ReactDevToolsHook | null = null\nlet chainedOriginal: ReactDevToolsHook['onCommitFiberRoot'] | null = null\n\nfunction ensureHookInstalled(): void {\n const g = globalThis as GlobalWithHook\n // Already ours and still installed — nothing to do.\n if (ourHook && g[HOOK_KEY] === ourHook) return\n\n const existing = g[HOOK_KEY]\n chainedOriginal =\n existing && existing !== ourHook && typeof existing.onCommitFiberRoot === 'function'\n ? existing.onCommitFiberRoot\n : null\n\n const hook: ReactDevToolsHook = existing && existing !== ourHook ? existing : {}\n // React's injectInternals() checks for supportsFiber and calls inject() to\n // get a renderer ID. Without these, React stores injectedHook = null and\n // never calls onCommitFiberRoot on subsequent commits.\n if (!hook.supportsFiber) {\n hook.supportsFiber = true\n }\n if (typeof hook.inject !== 'function') {\n let _nextRendererId = 1\n hook.inject = () => _nextRendererId++\n }\n // Additional fields that React Refresh (@vitejs/plugin-react) accesses on\n // the hook. Without these it throws \"Cannot read properties of undefined\n // (reading 'forEach')\" at injectIntoGlobalHook. We default each to a safe\n // empty/noop value; if the real React DevTools extension installs later\n // it will overwrite these.\n if (!hook.renderers) {\n ;(hook as Record<string, unknown>).renderers = new Map()\n }\n if (typeof (hook as Record<string, unknown>)['on'] !== 'function') {\n ;(hook as Record<string, unknown>)['on'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['off'] !== 'function') {\n ;(hook as Record<string, unknown>)['off'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['emit'] !== 'function') {\n ;(hook as Record<string, unknown>)['emit'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['sub'] !== 'function') {\n ;(hook as Record<string, unknown>)['sub'] = () => () => {}\n }\n if (typeof (hook as Record<string, unknown>)['checkDCE'] !== 'function') {\n ;(hook as Record<string, unknown>)['checkDCE'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['onCommitFiberUnmount'] !== 'function') {\n ;(hook as Record<string, unknown>)['onCommitFiberUnmount'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['onPostCommitFiberRoot'] !== 'function') {\n ;(hook as Record<string, unknown>)['onPostCommitFiberRoot'] = () => {}\n }\n hook.onCommitFiberRoot = (rendererId, root, priorityLevel) => {\n if (chainedOriginal) {\n try {\n chainedOriginal(rendererId, root, priorityLevel)\n } catch (err) {\n console.warn('[react-perfscope] chained DevTools hook threw:', err)\n }\n }\n for (const cb of listeners) {\n try {\n cb(root, rendererId)\n } catch (err) {\n console.warn('[react-perfscope] commit listener threw:', err)\n }\n }\n }\n g[HOOK_KEY] = hook\n ourHook = hook\n}\n\nexport function installDevToolsHook(listener: CommitListener): () => void {\n ensureHookInstalled()\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\n/**\n * Clears all listeners and forgets our installed hook reference. Used by\n * tests to fully reset module state. Does NOT remove the hook from\n * globalThis — callers that want a fully clean slate must also\n * `delete globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__`.\n */\nexport function uninstallDevToolsHook(): void {\n listeners.clear()\n ourHook = null\n chainedOriginal = null\n}\n","import type { MinimalFiber } from './types'\n\nconst MEMO_TYPE = Symbol.for('react.memo')\nconst FORWARD_REF_TYPE = Symbol.for('react.forward_ref')\n\ntype WrapperType = {\n $$typeof: symbol\n type?: unknown\n render?: unknown\n}\n\nfunction namedFunctionName(value: unknown): string | null {\n if (typeof value !== 'function') return null\n const fn = value as { displayName?: string; name?: string }\n if (typeof fn.displayName === 'string' && fn.displayName.length > 0) return fn.displayName\n if (typeof fn.name === 'string' && fn.name.length > 0) return fn.name\n return null\n}\n\n/**\n * Return the component name for a fiber.\n *\n * Host components (DOM tags) return their tag string. Function and class\n * components return their displayName or name. Memo and forwardRef wrappers\n * are unwrapped to their inner component. Unknown shapes return null.\n */\nexport function fiberComponentName(fiber: MinimalFiber | null): string | null {\n if (!fiber) return null\n const type = fiber.type\n if (typeof type === 'string') return type\n if (typeof type === 'function') return namedFunctionName(type)\n if (type && typeof type === 'object') {\n const wrapper = type as WrapperType\n if (wrapper.$$typeof === MEMO_TYPE) {\n return namedFunctionName(wrapper.type)\n }\n if (wrapper.$$typeof === FORWARD_REF_TYPE) {\n return namedFunctionName(wrapper.render)\n }\n }\n return null\n}\n\ninterface WalkOptions {\n /**\n * Maximum number of fibers to visit. Prevents runaway traversal on very\n * deep trees. Default 10_000.\n */\n stopAt?: number\n /**\n * Predicate deciding whether to descend into a fiber's children. Returning\n * false prunes the entire subtree (the fiber itself is still visited). Used\n * to skip subtrees that did no work this commit. Defaults to always descend.\n */\n descend?: (fiber: MinimalFiber) => boolean\n}\n\n/**\n * Walk a fiber subtree depth-first, invoking `visit` for every fiber with its\n * depth relative to `root` (root is depth 0). Returns early once `stopAt`\n * fibers have been visited. When `descend` returns false for a fiber, its\n * children are skipped entirely.\n */\nexport function walkChangedFibers(\n root: MinimalFiber,\n visit: (fiber: MinimalFiber, depth: number) => void,\n opts: WalkOptions = {}\n): void {\n const max = opts.stopAt ?? 10_000\n const descend = opts.descend ?? (() => true)\n let count = 0\n let depth = 0\n let node: MinimalFiber | null = root\n while (node) {\n visit(node, depth)\n count++\n if (count >= max) return\n if (node.child && descend(node)) {\n node = node.child\n depth++\n continue\n }\n while (node && !node.sibling) {\n if (node === root) return\n node = node.return\n depth--\n }\n if (!node || node === root) return\n node = node.sibling\n }\n}\n","import type { MinimalFiber } from './types'\nimport { fiberComponentName } from './fiber-walker'\n\nconst FIBER_KEY_PATTERNS = ['__reactFiber$', '__reactInternalInstance$']\n\nfunction findFiberOnElement(el: HTMLElement): MinimalFiber | null {\n for (const key of Object.keys(el)) {\n for (const pattern of FIBER_KEY_PATTERNS) {\n if (key.startsWith(pattern)) {\n return (el as HTMLElement & Record<string, MinimalFiber>)[key] ?? null\n }\n }\n }\n return null\n}\n\n/**\n * Walk up from the fiber attached to `el` until we find one whose `type` is\n * a function or class component (not a host tag string). Returns that\n * component's display name. If no component is found, returns the host\n * tag name of the starting fiber. If no fiber is attached, returns null.\n */\nexport function resolveComponentFromElement(el: HTMLElement): string | null {\n const start = findFiberOnElement(el)\n if (!start) return null\n let node: MinimalFiber | null = start\n while (node) {\n if (typeof node.type === 'function' || (node.type && typeof node.type === 'object')) {\n const name = fiberComponentName(node)\n if (name) return name\n }\n node = node.return\n }\n // Nothing but host fibers above — return the host tag name of the starting fiber.\n return fiberComponentName(start)\n}\n","import type { MinimalFiber } from './types'\n\n/** React's `PerformedWork` flag — set on a fiber whose render actually ran. */\nexport const PERFORMED_WORK = 0b1\n\nexport type RenderReason = 'mount' | 'state' | 'props' | 'parent'\n\nexport interface RenderReasonResult {\n reason: RenderReason\n changedProps?: string[]\n}\n\nexport function didPerformWork(fiber: MinimalFiber | null): boolean {\n if (!fiber || typeof fiber.flags !== 'number') return false\n return (fiber.flags & PERFORMED_WORK) !== 0\n}\n\n/**\n * Should the walk descend into this fiber's children when hunting for renders?\n *\n * React bubbles the `PerformedWork` bit of every descendant into a fiber's\n * `subtreeFlags`, and (unlike `flags`) resets it whenever the reconciler\n * re-clones the fiber. So a numeric `subtreeFlags` without the bit reliably\n * means \"no descendant rendered this commit\" — we can stop here and avoid\n * stale leaves whose own `flags` never got cleared.\n *\n * When `subtreeFlags` is absent (React < 18) we cannot prune safely, so we\n * descend and fall back to the full-tree walk.\n */\nexport function subtreeMightHaveRendered(fiber: MinimalFiber): boolean {\n if (typeof fiber.subtreeFlags !== 'number') return true\n return (fiber.subtreeFlags & PERFORMED_WORK) !== 0\n}\n\n/**\n * Walk up `.return` to the nearest *component* fiber, skipping host (DOM)\n * fibers. A component's immediate `.return` is usually a host fiber (the\n * `<div>` it's nested in), not its parent component — so to ask \"did my\n * parent component render?\" we have to climb past the host nodes first.\n */\nexport function nearestComponentAncestor(fiber: MinimalFiber): MinimalFiber | null {\n let p = fiber.return\n while (p) {\n if (typeof p.type === 'function') return p\n p = p.return\n }\n return null\n}\n\nfunction asObject(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' ? (value as Record<string, unknown>) : {}\n}\n\n/** Shallow diff of two prop bags. Returns the keys whose values differ. */\nexport function changedPropKeys(prev: unknown, next: unknown): string[] {\n if (prev === next) return []\n const p = asObject(prev)\n const n = asObject(next)\n const keys = new Set([...Object.keys(p), ...Object.keys(n)])\n const changed: string[] = []\n for (const k of keys) {\n if (p[k] !== n[k]) changed.push(k)\n }\n return changed\n}\n\n/**\n * Classify WHY a fiber that performed work this commit rendered.\n *\n * - `mount` — first render (no previous fiber).\n * - `props` — at least one prop changed since the previous render.\n * - `state` — props are identical AND the parent did not render, so the\n * update originated here (own state/force update). This is the\n * root of a render cascade.\n * - `parent` — props are identical but the parent rendered, so this only\n * re-rendered because of its parent. This is the avoidable\n * \"wrap me in memo\" case the developer is hunting for.\n *\n * Assumes the caller only invokes this for fibers that actually performed\n * work (see `didPerformWork`).\n */\nexport function classifyRenderReason(fiber: MinimalFiber): RenderReasonResult {\n if (!fiber.alternate) return { reason: 'mount' }\n const changed = changedPropKeys(fiber.alternate.memoizedProps, fiber.memoizedProps)\n if (changed.length > 0) return { reason: 'props', changedProps: changed }\n if (didPerformWork(nearestComponentAncestor(fiber))) return { reason: 'parent' }\n return { reason: 'state' }\n}\n","import type { Collector, Signal } from '@react-perfscope/core'\nimport { installDevToolsHook } from './devtools-hook'\nimport { fiberComponentName, walkChangedFibers } from './fiber-walker'\nimport { classifyRenderReason, didPerformWork, subtreeMightHaveRendered } from './render-reason'\nimport type { MinimalFiber } from './types'\n\nexport function createRenderCollector(): Collector {\n let active = false\n let emit: (signal: Signal) => void = () => {}\n let unsubscribe: (() => void) | null = null\n let commitId = 0\n\n function onCommit(root: { current: MinimalFiber }) {\n if (!active) return\n const at = performance.now()\n const id = commitId++\n walkChangedFibers(\n root.current,\n (fiber, depth) => {\n // Skip host (DOM) fibers — we only want function/class components in\n // render reports.\n if (typeof fiber.type === 'string') return\n // Only report fibers that actually re-ran their render this commit.\n // A bailed-out fiber (e.g. memo with equal props) keeps its place in\n // the tree but did no work, so reporting it would be noise.\n if (!didPerformWork(fiber)) return\n const name = fiberComponentName(fiber)\n if (!name) return\n const duration = typeof fiber.actualDuration === 'number' ? fiber.actualDuration : 0\n const { reason, changedProps } = classifyRenderReason(fiber)\n emit({\n kind: 'render',\n at,\n component: name,\n reason,\n duration,\n commitId: id,\n depth,\n ...(changedProps ? { changedProps } : {}),\n })\n },\n // Prune subtrees that did no work this commit. Without this, a leaf that\n // mounted long ago keeps its stale PerformedWork flag and gets reported\n // as a phantom render on every unrelated commit.\n { descend: subtreeMightHaveRendered }\n )\n }\n\n return {\n kind: 'render',\n activate(emitFn) {\n emit = emitFn\n active = true\n if (unsubscribe) return\n unsubscribe = installDevToolsHook(onCommit)\n },\n deactivate() {\n active = false\n // Keep the global hook listener attached — installDevToolsHook is\n // idempotent and removing it on every deactivate would cost us the\n // ability to re-attach cleanly under the test reset hooks. The\n // `active` flag gates emission.\n },\n }\n}\n"],"mappings":";AAIA,IAAM,WAAW;AAMjB,IAAM,YAAY,oBAAI,IAAoB;AAC1C,IAAI,UAAoC;AACxC,IAAI,kBAAiE;AAErE,SAAS,sBAA4B;AACnC,QAAM,IAAI;AAEV,MAAI,WAAW,EAAE,QAAQ,MAAM,QAAS;AAExC,QAAM,WAAW,EAAE,QAAQ;AAC3B,oBACE,YAAY,aAAa,WAAW,OAAO,SAAS,sBAAsB,aACtE,SAAS,oBACT;AAEN,QAAM,OAA0B,YAAY,aAAa,UAAU,WAAW,CAAC;AAI/E,MAAI,CAAC,KAAK,eAAe;AACvB,SAAK,gBAAgB;AAAA,EACvB;AACA,MAAI,OAAO,KAAK,WAAW,YAAY;AACrC,QAAI,kBAAkB;AACtB,SAAK,SAAS,MAAM;AAAA,EACtB;AAMA,MAAI,CAAC,KAAK,WAAW;AACnB;AAAC,IAAC,KAAiC,YAAY,oBAAI,IAAI;AAAA,EACzD;AACA,MAAI,OAAQ,KAAiC,IAAI,MAAM,YAAY;AACjE;AAAC,IAAC,KAAiC,IAAI,IAAI,MAAM;AAAA,IAAC;AAAA,EACpD;AACA,MAAI,OAAQ,KAAiC,KAAK,MAAM,YAAY;AAClE;AAAC,IAAC,KAAiC,KAAK,IAAI,MAAM;AAAA,IAAC;AAAA,EACrD;AACA,MAAI,OAAQ,KAAiC,MAAM,MAAM,YAAY;AACnE;AAAC,IAAC,KAAiC,MAAM,IAAI,MAAM;AAAA,IAAC;AAAA,EACtD;AACA,MAAI,OAAQ,KAAiC,KAAK,MAAM,YAAY;AAClE;AAAC,IAAC,KAAiC,KAAK,IAAI,MAAM,MAAM;AAAA,IAAC;AAAA,EAC3D;AACA,MAAI,OAAQ,KAAiC,UAAU,MAAM,YAAY;AACvE;AAAC,IAAC,KAAiC,UAAU,IAAI,MAAM;AAAA,IAAC;AAAA,EAC1D;AACA,MAAI,OAAQ,KAAiC,sBAAsB,MAAM,YAAY;AACnF;AAAC,IAAC,KAAiC,sBAAsB,IAAI,MAAM;AAAA,IAAC;AAAA,EACtE;AACA,MAAI,OAAQ,KAAiC,uBAAuB,MAAM,YAAY;AACpF;AAAC,IAAC,KAAiC,uBAAuB,IAAI,MAAM;AAAA,IAAC;AAAA,EACvE;AACA,OAAK,oBAAoB,CAAC,YAAY,MAAM,kBAAkB;AAC5D,QAAI,iBAAiB;AACnB,UAAI;AACF,wBAAgB,YAAY,MAAM,aAAa;AAAA,MACjD,SAAS,KAAK;AACZ,gBAAQ,KAAK,kDAAkD,GAAG;AAAA,MACpE;AAAA,IACF;AACA,eAAW,MAAM,WAAW;AAC1B,UAAI;AACF,WAAG,MAAM,UAAU;AAAA,MACrB,SAAS,KAAK;AACZ,gBAAQ,KAAK,4CAA4C,GAAG;AAAA,MAC9D;AAAA,IACF;AAAA,EACF;AACA,IAAE,QAAQ,IAAI;AACd,YAAU;AACZ;AAEO,SAAS,oBAAoB,UAAsC;AACxE,sBAAoB;AACpB,YAAU,IAAI,QAAQ;AACtB,SAAO,MAAM;AACX,cAAU,OAAO,QAAQ;AAAA,EAC3B;AACF;AAQO,SAAS,wBAA8B;AAC5C,YAAU,MAAM;AAChB,YAAU;AACV,oBAAkB;AACpB;;;ACrGA,IAAM,YAAY,uBAAO,IAAI,YAAY;AACzC,IAAM,mBAAmB,uBAAO,IAAI,mBAAmB;AAQvD,SAAS,kBAAkB,OAA+B;AACxD,MAAI,OAAO,UAAU,WAAY,QAAO;AACxC,QAAM,KAAK;AACX,MAAI,OAAO,GAAG,gBAAgB,YAAY,GAAG,YAAY,SAAS,EAAG,QAAO,GAAG;AAC/E,MAAI,OAAO,GAAG,SAAS,YAAY,GAAG,KAAK,SAAS,EAAG,QAAO,GAAG;AACjE,SAAO;AACT;AASO,SAAS,mBAAmB,OAA2C;AAC5E,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,OAAO,MAAM;AACnB,MAAI,OAAO,SAAS,SAAU,QAAO;AACrC,MAAI,OAAO,SAAS,WAAY,QAAO,kBAAkB,IAAI;AAC7D,MAAI,QAAQ,OAAO,SAAS,UAAU;AACpC,UAAM,UAAU;AAChB,QAAI,QAAQ,aAAa,WAAW;AAClC,aAAO,kBAAkB,QAAQ,IAAI;AAAA,IACvC;AACA,QAAI,QAAQ,aAAa,kBAAkB;AACzC,aAAO,kBAAkB,QAAQ,MAAM;AAAA,IACzC;AAAA,EACF;AACA,SAAO;AACT;AAsBO,SAAS,kBACd,MACA,OACA,OAAoB,CAAC,GACf;AACN,QAAM,MAAM,KAAK,UAAU;AAC3B,QAAM,UAAU,KAAK,YAAY,MAAM;AACvC,MAAI,QAAQ;AACZ,MAAI,QAAQ;AACZ,MAAI,OAA4B;AAChC,SAAO,MAAM;AACX,UAAM,MAAM,KAAK;AACjB;AACA,QAAI,SAAS,IAAK;AAClB,QAAI,KAAK,SAAS,QAAQ,IAAI,GAAG;AAC/B,aAAO,KAAK;AACZ;AACA;AAAA,IACF;AACA,WAAO,QAAQ,CAAC,KAAK,SAAS;AAC5B,UAAI,SAAS,KAAM;AACnB,aAAO,KAAK;AACZ;AAAA,IACF;AACA,QAAI,CAAC,QAAQ,SAAS,KAAM;AAC5B,WAAO,KAAK;AAAA,EACd;AACF;;;ACvFA,IAAM,qBAAqB,CAAC,iBAAiB,0BAA0B;AAEvE,SAAS,mBAAmB,IAAsC;AAChE,aAAW,OAAO,OAAO,KAAK,EAAE,GAAG;AACjC,eAAW,WAAW,oBAAoB;AACxC,UAAI,IAAI,WAAW,OAAO,GAAG;AAC3B,eAAQ,GAAkD,GAAG,KAAK;AAAA,MACpE;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAQO,SAAS,4BAA4B,IAAgC;AAC1E,QAAM,QAAQ,mBAAmB,EAAE;AACnC,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI,OAA4B;AAChC,SAAO,MAAM;AACX,QAAI,OAAO,KAAK,SAAS,cAAe,KAAK,QAAQ,OAAO,KAAK,SAAS,UAAW;AACnF,YAAM,OAAO,mBAAmB,IAAI;AACpC,UAAI,KAAM,QAAO;AAAA,IACnB;AACA,WAAO,KAAK;AAAA,EACd;AAEA,SAAO,mBAAmB,KAAK;AACjC;;;AChCO,IAAM,iBAAiB;AASvB,SAAS,eAAe,OAAqC;AAClE,MAAI,CAAC,SAAS,OAAO,MAAM,UAAU,SAAU,QAAO;AACtD,UAAQ,MAAM,QAAQ,oBAAoB;AAC5C;AAcO,SAAS,yBAAyB,OAA8B;AACrE,MAAI,OAAO,MAAM,iBAAiB,SAAU,QAAO;AACnD,UAAQ,MAAM,eAAe,oBAAoB;AACnD;AAQO,SAAS,yBAAyB,OAA0C;AACjF,MAAI,IAAI,MAAM;AACd,SAAO,GAAG;AACR,QAAI,OAAO,EAAE,SAAS,WAAY,QAAO;AACzC,QAAI,EAAE;AAAA,EACR;AACA,SAAO;AACT;AAEA,SAAS,SAAS,OAAyC;AACzD,SAAO,SAAS,OAAO,UAAU,WAAY,QAAoC,CAAC;AACpF;AAGO,SAAS,gBAAgB,MAAe,MAAyB;AACtE,MAAI,SAAS,KAAM,QAAO,CAAC;AAC3B,QAAM,IAAI,SAAS,IAAI;AACvB,QAAM,IAAI,SAAS,IAAI;AACvB,QAAM,OAAO,oBAAI,IAAI,CAAC,GAAG,OAAO,KAAK,CAAC,GAAG,GAAG,OAAO,KAAK,CAAC,CAAC,CAAC;AAC3D,QAAM,UAAoB,CAAC;AAC3B,aAAW,KAAK,MAAM;AACpB,QAAI,EAAE,CAAC,MAAM,EAAE,CAAC,EAAG,SAAQ,KAAK,CAAC;AAAA,EACnC;AACA,SAAO;AACT;AAiBO,SAAS,qBAAqB,OAAyC;AAC5E,MAAI,CAAC,MAAM,UAAW,QAAO,EAAE,QAAQ,QAAQ;AAC/C,QAAM,UAAU,gBAAgB,MAAM,UAAU,eAAe,MAAM,aAAa;AAClF,MAAI,QAAQ,SAAS,EAAG,QAAO,EAAE,QAAQ,SAAS,cAAc,QAAQ;AACxE,MAAI,eAAe,yBAAyB,KAAK,CAAC,EAAG,QAAO,EAAE,QAAQ,SAAS;AAC/E,SAAO,EAAE,QAAQ,QAAQ;AAC3B;;;ACjFO,SAAS,wBAAmC;AACjD,MAAI,SAAS;AACb,MAAI,OAAiC,MAAM;AAAA,EAAC;AAC5C,MAAI,cAAmC;AACvC,MAAI,WAAW;AAEf,WAAS,SAAS,MAAiC;AACjD,QAAI,CAAC,OAAQ;AACb,UAAM,KAAK,YAAY,IAAI;AAC3B,UAAM,KAAK;AACX;AAAA,MACE,KAAK;AAAA,MACL,CAAC,OAAO,UAAU;AAGhB,YAAI,OAAO,MAAM,SAAS,SAAU;AAIpC,YAAI,CAAC,eAAe,KAAK,EAAG;AAC5B,cAAM,OAAO,mBAAmB,KAAK;AACrC,YAAI,CAAC,KAAM;AACX,cAAM,WAAW,OAAO,MAAM,mBAAmB,WAAW,MAAM,iBAAiB;AACnF,cAAM,EAAE,QAAQ,aAAa,IAAI,qBAAqB,KAAK;AAC3D,aAAK;AAAA,UACH,MAAM;AAAA,UACN;AAAA,UACA,WAAW;AAAA,UACX;AAAA,UACA;AAAA,UACA,UAAU;AAAA,UACV;AAAA,UACA,GAAI,eAAe,EAAE,aAAa,IAAI,CAAC;AAAA,QACzC,CAAC;AAAA,MACH;AAAA;AAAA;AAAA;AAAA,MAIA,EAAE,SAAS,yBAAyB;AAAA,IACtC;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS,QAAQ;AACf,aAAO;AACP,eAAS;AACT,UAAI,YAAa;AACjB,oBAAc,oBAAoB,QAAQ;AAAA,IAC5C;AAAA,IACA,aAAa;AACX,eAAS;AAAA,IAKX;AAAA,EACF;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/devtools-hook.ts","../src/fiber-walker.ts","../src/attribution.ts","../src/render-reason.ts","../src/render-collector.ts"],"sourcesContent":["import type { MinimalFiber, ReactDevToolsHook } from './types'\n\ntype CommitListener = (root: { current: MinimalFiber }, rendererId: number) => void\n\nconst HOOK_KEY = '__REACT_DEVTOOLS_GLOBAL_HOOK__'\n\ninterface GlobalWithHook {\n [HOOK_KEY]?: ReactDevToolsHook\n}\n\nconst listeners = new Set<CommitListener>()\nlet ourHook: ReactDevToolsHook | null = null\nlet chainedOriginal: ReactDevToolsHook['onCommitFiberRoot'] | null = null\n\nfunction ensureHookInstalled(): void {\n const g = globalThis as GlobalWithHook\n // Already ours and still installed — nothing to do.\n if (ourHook && g[HOOK_KEY] === ourHook) return\n\n const existing = g[HOOK_KEY]\n chainedOriginal =\n existing && existing !== ourHook && typeof existing.onCommitFiberRoot === 'function'\n ? existing.onCommitFiberRoot\n : null\n\n const hook: ReactDevToolsHook = existing && existing !== ourHook ? existing : {}\n // React's injectInternals() checks for supportsFiber and calls inject() to\n // get a renderer ID. Without these, React stores injectedHook = null and\n // never calls onCommitFiberRoot on subsequent commits.\n if (!hook.supportsFiber) {\n hook.supportsFiber = true\n }\n if (typeof hook.inject !== 'function') {\n let _nextRendererId = 1\n hook.inject = () => _nextRendererId++\n }\n // Additional fields that React Refresh (@vitejs/plugin-react) accesses on\n // the hook. Without these it throws \"Cannot read properties of undefined\n // (reading 'forEach')\" at injectIntoGlobalHook. We default each to a safe\n // empty/noop value; if the real React DevTools extension installs later\n // it will overwrite these.\n if (!hook.renderers) {\n ;(hook as Record<string, unknown>).renderers = new Map()\n }\n if (typeof (hook as Record<string, unknown>)['on'] !== 'function') {\n ;(hook as Record<string, unknown>)['on'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['off'] !== 'function') {\n ;(hook as Record<string, unknown>)['off'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['emit'] !== 'function') {\n ;(hook as Record<string, unknown>)['emit'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['sub'] !== 'function') {\n ;(hook as Record<string, unknown>)['sub'] = () => () => {}\n }\n if (typeof (hook as Record<string, unknown>)['checkDCE'] !== 'function') {\n ;(hook as Record<string, unknown>)['checkDCE'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['onCommitFiberUnmount'] !== 'function') {\n ;(hook as Record<string, unknown>)['onCommitFiberUnmount'] = () => {}\n }\n if (typeof (hook as Record<string, unknown>)['onPostCommitFiberRoot'] !== 'function') {\n ;(hook as Record<string, unknown>)['onPostCommitFiberRoot'] = () => {}\n }\n hook.onCommitFiberRoot = (rendererId, root, priorityLevel) => {\n if (chainedOriginal) {\n try {\n chainedOriginal(rendererId, root, priorityLevel)\n } catch (err) {\n console.warn('[react-perfscope] chained DevTools hook threw:', err)\n }\n }\n for (const cb of listeners) {\n try {\n cb(root, rendererId)\n } catch (err) {\n console.warn('[react-perfscope] commit listener threw:', err)\n }\n }\n }\n g[HOOK_KEY] = hook\n ourHook = hook\n}\n\nexport function installDevToolsHook(listener: CommitListener): () => void {\n ensureHookInstalled()\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\n/**\n * Clears all listeners and forgets our installed hook reference. Used by\n * tests to fully reset module state. Does NOT remove the hook from\n * globalThis — callers that want a fully clean slate must also\n * `delete globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__`.\n */\nexport function uninstallDevToolsHook(): void {\n listeners.clear()\n ourHook = null\n chainedOriginal = null\n}\n","import type { MinimalFiber } from './types'\n\nconst MEMO_TYPE = Symbol.for('react.memo')\nconst FORWARD_REF_TYPE = Symbol.for('react.forward_ref')\n\ntype WrapperType = {\n $$typeof: symbol\n type?: unknown\n render?: unknown\n}\n\nfunction namedFunctionName(value: unknown): string | null {\n if (typeof value !== 'function') return null\n const fn = value as { displayName?: string; name?: string }\n if (typeof fn.displayName === 'string' && fn.displayName.length > 0) return fn.displayName\n if (typeof fn.name === 'string' && fn.name.length > 0) return fn.name\n return null\n}\n\n/**\n * Return the component name for a fiber.\n *\n * Host components (DOM tags) return their tag string. Function and class\n * components return their displayName or name. Memo and forwardRef wrappers\n * are unwrapped to their inner component. Unknown shapes return null.\n */\nexport function fiberComponentName(fiber: MinimalFiber | null): string | null {\n if (!fiber) return null\n const type = fiber.type\n if (typeof type === 'string') return type\n if (typeof type === 'function') return namedFunctionName(type)\n if (type && typeof type === 'object') {\n const wrapper = type as WrapperType\n if (wrapper.$$typeof === MEMO_TYPE) {\n return namedFunctionName(wrapper.type)\n }\n if (wrapper.$$typeof === FORWARD_REF_TYPE) {\n return namedFunctionName(wrapper.render)\n }\n }\n return null\n}\n\ninterface WalkOptions {\n /**\n * Maximum number of fibers to visit. Prevents runaway traversal on very\n * deep trees. Default 10_000.\n */\n stopAt?: number\n /**\n * Predicate deciding whether to descend into a fiber's children. Returning\n * false prunes the entire subtree (the fiber itself is still visited). Used\n * to skip subtrees that did no work this commit. Defaults to always descend.\n */\n descend?: (fiber: MinimalFiber) => boolean\n}\n\n/**\n * Walk a fiber subtree depth-first, invoking `visit` for every fiber with its\n * depth relative to `root` (root is depth 0). Returns early once `stopAt`\n * fibers have been visited. When `descend` returns false for a fiber, its\n * children are skipped entirely.\n */\nexport function walkChangedFibers(\n root: MinimalFiber,\n visit: (fiber: MinimalFiber, depth: number) => void,\n opts: WalkOptions = {}\n): void {\n const max = opts.stopAt ?? 10_000\n const descend = opts.descend ?? (() => true)\n let count = 0\n let depth = 0\n let node: MinimalFiber | null = root\n while (node) {\n visit(node, depth)\n count++\n if (count >= max) return\n if (node.child && descend(node)) {\n node = node.child\n depth++\n continue\n }\n while (node && !node.sibling) {\n if (node === root) return\n node = node.return\n depth--\n }\n if (!node || node === root) return\n node = node.sibling\n }\n}\n","import type { MinimalFiber } from './types'\nimport { fiberComponentName } from './fiber-walker'\n\nconst FIBER_KEY_PATTERNS = ['__reactFiber$', '__reactInternalInstance$']\n\nfunction findFiberOnElement(el: HTMLElement): MinimalFiber | null {\n for (const key of Object.keys(el)) {\n for (const pattern of FIBER_KEY_PATTERNS) {\n if (key.startsWith(pattern)) {\n return (el as HTMLElement & Record<string, MinimalFiber>)[key] ?? null\n }\n }\n }\n return null\n}\n\n/**\n * Walk up from the fiber attached to `el` until we find one whose `type` is\n * a function or class component (not a host tag string). Returns that\n * component's display name. If no component is found, returns the host\n * tag name of the starting fiber. If no fiber is attached, returns null.\n */\nexport function resolveComponentFromElement(el: HTMLElement): string | null {\n const start = findFiberOnElement(el)\n if (!start) return null\n let node: MinimalFiber | null = start\n while (node) {\n if (typeof node.type === 'function' || (node.type && typeof node.type === 'object')) {\n const name = fiberComponentName(node)\n if (name) return name\n }\n node = node.return\n }\n // Nothing but host fibers above — return the host tag name of the starting fiber.\n return fiberComponentName(start)\n}\n","import type { MinimalFiber } from './types'\n\n/** React's `PerformedWork` flag — set on a fiber whose render actually ran. */\nexport const PERFORMED_WORK = 0b1\n\nexport type RenderReason = 'mount' | 'state' | 'props' | 'parent'\n\nexport interface RenderReasonResult {\n reason: RenderReason\n changedProps?: string[]\n}\n\nexport function didPerformWork(fiber: MinimalFiber | null): boolean {\n if (!fiber || typeof fiber.flags !== 'number') return false\n return (fiber.flags & PERFORMED_WORK) !== 0\n}\n\n/**\n * Should the walk descend into this fiber's children when hunting for renders?\n *\n * React bubbles the `PerformedWork` bit of every descendant into a fiber's\n * `subtreeFlags`, and (unlike `flags`) resets it whenever the reconciler\n * re-clones the fiber. So a numeric `subtreeFlags` without the bit reliably\n * means \"no descendant rendered this commit\" — we can stop here and avoid\n * stale leaves whose own `flags` never got cleared.\n *\n * When `subtreeFlags` is absent (React < 18) we cannot prune safely, so we\n * descend and fall back to the full-tree walk.\n */\nexport function subtreeMightHaveRendered(fiber: MinimalFiber): boolean {\n if (typeof fiber.subtreeFlags !== 'number') return true\n return (fiber.subtreeFlags & PERFORMED_WORK) !== 0\n}\n\n/**\n * Walk up `.return` to the nearest *component* fiber, skipping host (DOM)\n * fibers. A component's immediate `.return` is usually a host fiber (the\n * `<div>` it's nested in), not its parent component — so to ask \"did my\n * parent component render?\" we have to climb past the host nodes first.\n */\nexport function nearestComponentAncestor(fiber: MinimalFiber): MinimalFiber | null {\n let p = fiber.return\n while (p) {\n if (typeof p.type === 'function') return p\n p = p.return\n }\n return null\n}\n\nfunction asObject(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' ? (value as Record<string, unknown>) : {}\n}\n\n/** Shallow diff of two prop bags. Returns the keys whose values differ. */\nexport function changedPropKeys(prev: unknown, next: unknown): string[] {\n if (prev === next) return []\n const p = asObject(prev)\n const n = asObject(next)\n const keys = new Set([...Object.keys(p), ...Object.keys(n)])\n const changed: string[] = []\n for (const k of keys) {\n if (p[k] !== n[k]) changed.push(k)\n }\n return changed\n}\n\n/**\n * Classify WHY a fiber that performed work this commit rendered.\n *\n * - `mount` — first render (no previous fiber).\n * - `props` — at least one prop changed since the previous render.\n * - `state` — props are identical AND the parent did not render, so the\n * update originated here (own state/force update). This is the\n * root of a render cascade.\n * - `parent` — props are identical but the parent rendered, so this only\n * re-rendered because of its parent. This is the avoidable\n * \"wrap me in memo\" case the developer is hunting for.\n *\n * Assumes the caller only invokes this for fibers that actually performed\n * work (see `didPerformWork`).\n */\nexport function classifyRenderReason(fiber: MinimalFiber): RenderReasonResult {\n if (!fiber.alternate) return { reason: 'mount' }\n const changed = changedPropKeys(fiber.alternate.memoizedProps, fiber.memoizedProps)\n if (changed.length > 0) return { reason: 'props', changedProps: changed }\n if (didPerformWork(nearestComponentAncestor(fiber))) return { reason: 'parent' }\n return { reason: 'state' }\n}\n","import type { Collector, RenderSignal, Signal } from '@react-perfscope/core'\nimport { installDevToolsHook } from './devtools-hook'\nimport { fiberComponentName, walkChangedFibers } from './fiber-walker'\nimport { classifyRenderReason, didPerformWork, subtreeMightHaveRendered } from './render-reason'\nimport type { MinimalFiber } from './types'\n\n/** The cascade root of a commit: the component whose own state/mount started\n * it. Falls back to the shallowest member. */\nfunction cascadeRoot(members: RenderSignal[]): RenderSignal {\n let shallowest = members[0]!\n for (const m of members) {\n if (m.reason === 'state') return m\n if (m.depth < shallowest.depth) shallowest = m\n }\n return members.find((m) => m.reason === 'mount') ?? shallowest\n}\n\nexport function createRenderCollector(): Collector {\n let active = false\n let emit: (signal: Signal) => void = () => {}\n let unsubscribe: (() => void) | null = null\n let commitId = 0\n\n function onCommit(root: { current: MinimalFiber }) {\n if (!active) return\n const at = performance.now()\n const id = commitId++\n const members: RenderSignal[] = []\n walkChangedFibers(\n root.current,\n (fiber, depth) => {\n // Skip host (DOM) fibers — we only want function/class components in\n // render reports.\n if (typeof fiber.type === 'string') return\n // Only report fibers that actually re-ran their render this commit.\n // A bailed-out fiber (e.g. memo with equal props) keeps its place in\n // the tree but did no work, so reporting it would be noise.\n if (!didPerformWork(fiber)) return\n const name = fiberComponentName(fiber)\n if (!name) return\n const duration = typeof fiber.actualDuration === 'number' ? fiber.actualDuration : 0\n const { reason, changedProps } = classifyRenderReason(fiber)\n members.push({\n kind: 'render',\n at,\n component: name,\n reason,\n duration,\n commitId: id,\n depth,\n ...(changedProps ? { changedProps } : {}),\n })\n },\n // Prune subtrees that did no work this commit. Without this, a leaf that\n // mounted long ago keeps its stale PerformedWork flag and gets reported\n // as a phantom render on every unrelated commit.\n { descend: subtreeMightHaveRendered }\n )\n if (members.length === 0) return\n // One coalesced signal per commit. The root describes the cascade; the\n // duration is the commit's total render time.\n const root2 = cascadeRoot(members)\n let total = 0\n for (const m of members) total += m.duration\n emit({\n kind: 'render',\n at,\n component: root2.component,\n reason: root2.reason,\n duration: total,\n commitId: id,\n depth: root2.depth,\n ...(root2.changedProps ? { changedProps: root2.changedProps } : {}),\n members,\n count: members.length,\n })\n }\n\n return {\n kind: 'render',\n activate(emitFn) {\n emit = emitFn\n active = true\n if (unsubscribe) return\n unsubscribe = installDevToolsHook(onCommit)\n },\n deactivate() {\n active = false\n // Keep the global hook listener attached — installDevToolsHook is\n // idempotent and removing it on every deactivate would cost us the\n // ability to re-attach cleanly under the test reset hooks. The\n // `active` flag gates emission.\n },\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-perfscope/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "React render collector for react-perfscope.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"dist"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@react-perfscope/core": "0.
|
|
28
|
+
"@react-perfscope/core": "0.4.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"react": "^18.3.0 || ^19.0.0",
|