diff --git a/packages/scan/package.json b/packages/scan/package.json index 2fb00d3a..a18bc65c 100644 --- a/packages/scan/package.json +++ b/packages/scan/package.json @@ -1,6 +1,6 @@ { "name": "react-scan", - "version": "0.1.1", + "version": "0.1.3", "description": "Scan your React app for renders", "keywords": [ "react", diff --git a/packages/scan/src/web/components/inspector/overlay/index.tsx b/packages/scan/src/web/components/inspector/overlay/index.tsx index 9bb164b7..5c21354d 100644 --- a/packages/scan/src/web/components/inspector/overlay/index.tsx +++ b/packages/scan/src/web/components/inspector/overlay/index.tsx @@ -11,7 +11,6 @@ import { import { signalIsSettingsOpen } from '~web/state'; import { cn, throttle } from '~web/utils/helpers'; import { lerp } from '~web/utils/lerp'; -import { timelineState } from '../states'; type DrawKind = 'locked' | 'inspecting'; @@ -101,24 +100,11 @@ export const ScanOverlay = () => { ) => { if (!fiber) return; - const currentUpdate = timelineState.value.updates[timelineState.value.currentIndex]; - - const stats = { - count: timelineState.value.updates.length - 1, - time: currentUpdate?.fiberInfo?.selfTime, - }; - const pillHeight = 24; const pillPadding = 8; const componentName = (fiber?.type && getDisplayName(fiber.type)) ?? 'Unknown'; - let text = componentName; - if (stats.count > 0) { - text += ` • ×${stats.count}`; - if (stats.time) { - text += ` (${stats.time.toFixed(1)}ms)`; - } - } + const text = componentName; ctx.save(); ctx.font = '12px system-ui, -apple-system, sans-serif'; diff --git a/packages/scan/src/web/components/inspector/states.ts b/packages/scan/src/web/components/inspector/states.ts index ffb2fb91..38ecda97 100644 --- a/packages/scan/src/web/components/inspector/states.ts +++ b/packages/scan/src/web/components/inspector/states.ts @@ -52,10 +52,62 @@ export const timelineState = signal(timelineStateDefault); export const inspectorUpdateSignal = signal(0); -// Add batching storage let pendingUpdates: Array<{ update: TimelineUpdate; fiber: Fiber | null }> = []; let batchTimeout: ReturnType | null = null; +const batchUpdates = () => { + if (pendingUpdates.length === 0) return; + + const batchedUpdates = [...pendingUpdates]; + + const { updates, totalUpdates, currentIndex, isViewingHistory } = + timelineState.value; + const newUpdates = [...updates]; + let newTotalUpdates = totalUpdates; + + for (const { update } of batchedUpdates) { + if (newUpdates.length >= TIMELINE_MAX_UPDATES) { + newUpdates.shift(); + } + newUpdates.push(update); + newTotalUpdates++; + } + + const newWindowOffset = Math.max(0, newTotalUpdates - TIMELINE_MAX_UPDATES); + + let newCurrentIndex: number; + if (isViewingHistory) { + if (currentIndex === totalUpdates - 1) { + newCurrentIndex = newUpdates.length - 1; + } else if (currentIndex === 0) { + newCurrentIndex = 0; + } else { + if (newWindowOffset === 0) { + newCurrentIndex = currentIndex; + } else { + newCurrentIndex = currentIndex - 1; + } + } + } else { + newCurrentIndex = newUpdates.length - 1; + } + + const lastUpdate = batchedUpdates[batchedUpdates.length - 1]; + + timelineState.value = { + ...timelineState.value, + latestFiber: lastUpdate.fiber, + updates: newUpdates, + totalUpdates: newTotalUpdates, + windowOffset: newWindowOffset, + currentIndex: newCurrentIndex, + isViewingHistory, + }; + + // Only after signal is updated, remove the processed updates + pendingUpdates = pendingUpdates.slice(batchedUpdates.length); +}; + export const timelineActions = { showTimeline: () => { timelineState.value = { @@ -88,67 +140,20 @@ export const timelineActions = { }, addUpdate: (update: TimelineUpdate, latestFiber: Fiber | null) => { - // Collect the update pendingUpdates.push({ update, fiber: latestFiber }); if (!batchTimeout) { - // If no batch is scheduled, schedule one - batchTimeout = setTimeout(() => { - // Process all collected updates - const batchedUpdates = pendingUpdates; - pendingUpdates = []; - batchTimeout = null; + const processBatch = () => { + batchUpdates(); - // Apply all updates in the batch - const { updates, totalUpdates, currentIndex, isViewingHistory } = - timelineState.value; - const newUpdates = [...updates]; - let newTotalUpdates = totalUpdates; - - // Process each update in the batch - for (const { update } of batchedUpdates) { - if (newUpdates.length >= TIMELINE_MAX_UPDATES) { - newUpdates.shift(); - } - newUpdates.push(update); - newTotalUpdates++; - } + batchTimeout = null; - const newWindowOffset = Math.max( - 0, - newTotalUpdates - TIMELINE_MAX_UPDATES, - ); - - let newCurrentIndex: number; - if (isViewingHistory) { - if (currentIndex === totalUpdates - 1) { - newCurrentIndex = newUpdates.length - 1; - } else if (currentIndex === 0) { - newCurrentIndex = 0; - } else { - if (newWindowOffset === 0) { - newCurrentIndex = currentIndex; - } else { - newCurrentIndex = currentIndex - 1; - } - } - } else { - newCurrentIndex = newUpdates.length - 1; + if (pendingUpdates.length > 0) { + batchTimeout = setTimeout(processBatch, 96); } + }; - // Get the latest fiber from the last update in batch - const lastUpdate = batchedUpdates[batchedUpdates.length - 1]; - - timelineState.value = { - ...timelineState.value, - latestFiber: lastUpdate.fiber, - updates: newUpdates, - totalUpdates: newTotalUpdates, - windowOffset: newWindowOffset, - currentIndex: newCurrentIndex, - isViewingHistory, - }; - }, 100); + batchTimeout = setTimeout(processBatch, 96); } }, diff --git a/packages/scan/src/web/components/inspector/timeline/utils.ts b/packages/scan/src/web/components/inspector/timeline/utils.ts index 7631ac48..1c7032a8 100644 --- a/packages/scan/src/web/components/inspector/timeline/utils.ts +++ b/packages/scan/src/web/components/inspector/timeline/utils.ts @@ -58,13 +58,30 @@ export const trackChange = ( previousValue: unknown, ): { hasChanged: boolean; count: number } => { const existing = tracker.get(key); + const isInitialValue = tracker === propsTracker || tracker === contextTracker; + const hasChanged = !isEqual(currentValue, previousValue); - if (!existing || !isEqual(existing.currentValue, currentValue)) { - const newCount = (existing?.count ?? 0) + 1; + if (!existing) { + // For props and context, start with count 1 if there's a change + tracker.set(key, { + count: hasChanged && isInitialValue ? 1 : 0, + currentValue, + previousValue, + lastUpdated: Date.now(), + }); + + return { + hasChanged, + count: hasChanged && isInitialValue ? 1 : isInitialValue ? 0 : 1, + }; + } + + if (!isEqual(existing.currentValue, currentValue)) { + const newCount = existing.count + 1; tracker.set(key, { count: newCount, currentValue, - previousValue: existing?.currentValue ?? previousValue, + previousValue: existing.currentValue, lastUpdated: Date.now(), }); return { hasChanged: true, count: newCount }; diff --git a/packages/scan/src/web/components/inspector/utils.ts b/packages/scan/src/web/components/inspector/utils.ts index 5faf9475..07cf5904 100644 --- a/packages/scan/src/web/components/inspector/utils.ts +++ b/packages/scan/src/web/components/inspector/utils.ts @@ -231,17 +231,41 @@ export const getCompositeFiberFromElement = ( let fiber = knownFiber ?? getNearestFiberFromElement(element); if (!fiber) return {}; - // Get the current associated fiber - fiber = isCurrentTree(fiber) ? fiber : (fiber.alternate ?? fiber); + // Find root once and cache it + let curr: Fiber | null = fiber; + let rootFiber: Fiber | null = null; + let currentRootFiber: Fiber | null = null; + + while (curr) { + if (!curr.stateNode) { + curr = curr.return; + continue; + } + if (ReactScanInternals.instrumentation?.fiberRoots.has(curr.stateNode)) { + rootFiber = curr; + currentRootFiber = curr.stateNode.current; + break; + } + curr = curr.return; + } + + if (!rootFiber || !currentRootFiber) return {}; + + // Get the current associated fiber using cached root + fiber = isFiberInTree(fiber, currentRootFiber) + ? fiber + : (fiber.alternate ?? fiber); + if (!fiber) return {}; if (!getFirstStateNode(fiber)) return {}; - // Fetch the parent composite fiber efficiently + // Get parent composite fiber const parentCompositeFiber = getParentCompositeFiber(fiber)?.[0]; if (!parentCompositeFiber) return {}; + // Use cached root to check parent fiber return { - parentCompositeFiber: isCurrentTree(parentCompositeFiber) + parentCompositeFiber: isFiberInTree(parentCompositeFiber, currentRootFiber) ? parentCompositeFiber : (parentCompositeFiber.alternate ?? parentCompositeFiber), }; diff --git a/packages/scan/src/web/components/inspector/what-changed.tsx b/packages/scan/src/web/components/inspector/what-changed.tsx index 55b997e1..aaf8911a 100644 --- a/packages/scan/src/web/components/inspector/what-changed.tsx +++ b/packages/scan/src/web/components/inspector/what-changed.tsx @@ -38,7 +38,6 @@ const safeGetValue = (value: unknown): { value: unknown; error?: string } => { } try { - // proxies or getter errors const proto = Object.getPrototypeOf(value); if (proto === Promise.prototype || proto?.constructor?.name === 'Promise') { return { value: 'Promise' }; @@ -88,6 +87,7 @@ export const WhatChangedSection = memo(() => { cancelAnimationFrame(rafId); }; }, []); + return ( <> { @@ -390,12 +390,11 @@ const Section = memo(({ title, isExpanded }: SectionProps) => { const currentData = currentUpdate?.[title.toLowerCase() as SectionType]; const prevData = prevUpdate?.[title.toLowerCase() as SectionType]; - if (!currentData || !prevData) { + if (!currentData) { return; } refFiberInfo.current = currentUpdate?.fiberInfo; - refLastUpdated.current.clear(); const changesMap = new Map( @@ -403,12 +402,17 @@ const Section = memo(({ title, isExpanded }: SectionProps) => { ); for (const { name, value } of currentData.current) { - const count = currentData.changesCounts?.get(name) ?? 0; - const prevValue = prevData.current.find( + const currentCount = currentData.changesCounts?.get(name) ?? 0; + const prevCount = prevData?.changesCounts?.get(name) ?? 0; + const count = Math.max(currentCount, prevCount); + + const prevValue = prevData?.current.find( (p) => p.name === name, )?.value; - if (!isEqual(value, prevValue) || count > 0) { + const hasValueChange = !isEqual(value, prevValue); + + if (count > 0 || hasValueChange) { const { value: safePrevValue, error: prevError } = safeGetValue(prevValue); const { value: safeCurrValue, error: currError } = diff --git a/packages/scan/src/web/components/widget/components-tree/index.tsx b/packages/scan/src/web/components/widget/components-tree/index.tsx index fa0fefd4..e40fc0b1 100644 --- a/packages/scan/src/web/components/widget/components-tree/index.tsx +++ b/packages/scan/src/web/components/widget/components-tree/index.tsx @@ -281,7 +281,7 @@ const TreeNodeItem = ({ )} )} - {wrapperTypes.length > 1 && `×${wrapperTypes.length - 1}`} + {wrapperTypes.length > 1 && `×${wrapperTypes.length}`} ); }, [node.fiber, typeHighlight]);