@@ -28,10 +28,7 @@ import {
2828} from 'src/core/utils' ;
2929import { readLocalStorage , saveLocalStorage } from '@web-utils/helpers' ;
3030import { initReactScanOverlay } from './web/overlay' ;
31- import {
32- createInstrumentation ,
33- type Render ,
34- } from './instrumentation' ;
31+ import { createInstrumentation , type Render } from './instrumentation' ;
3532import { createToolbar } from './web/toolbar' ;
3633import type { InternalInteraction } from './monitor/types' ;
3734import { type getSession } from './monitor/utils' ;
@@ -133,6 +130,24 @@ export interface Options {
133130 */
134131 animationSpeed ?: 'slow' | 'fast' | 'off' ;
135132
133+ /**
134+ * Smoothly animate the re-render outline when the element moves
135+ *
136+ * @default true
137+ */
138+ smoothlyAnimateOutlines ?: boolean ;
139+
140+ /**
141+ * Track unnecessary renders, and mark their outlines gray when detected
142+ *
143+ * An unnecessary render is defined as the component re-rendering with no change to the component's
144+ * corresponding dom subtree
145+ *
146+ * @default false
147+ * @warning tracking unnecessary renders can add meaningful overhead to react-scan
148+ */
149+ trackUnnecessaryRenders ?: boolean ;
150+
136151 onCommitStart ?: ( ) => void ;
137152 onRender ?: ( fiber : Fiber , renders : Array < Render > ) => void ;
138153 onCommitFinish ?: ( ) => void ;
@@ -217,6 +232,8 @@ export const ReactScanInternals: Internals = {
217232 alwaysShowLabels : false ,
218233 animationSpeed : 'fast' ,
219234 dangerouslyForceRunInProduction : false ,
235+ smoothlyAnimateOutlines : true ,
236+ trackUnnecessaryRenders : true ,
220237 } ) ,
221238 onRender : null ,
222239 scheduledOutlines : new Map ( ) ,
@@ -281,6 +298,17 @@ const validateOptions = (options: Partial<Options>): Partial<Options> => {
281298 ( validOptions as any ) [ key ] = value ;
282299 }
283300 break ;
301+ case 'trackUnnecessaryRenders' : {
302+ validOptions [ 'trackUnnecessaryRenders' ] =
303+ typeof value === 'boolean' ? value : false ;
304+ break ;
305+ }
306+
307+ case 'smoothlyAnimateOutlines' : {
308+ validOptions [ 'smoothlyAnimateOutlines' ] =
309+ typeof value === 'boolean' ? value : false ;
310+ break ;
311+ }
284312 default :
285313 errors . push ( `- Unknown option "${ key } "` ) ;
286314 }
@@ -417,6 +445,46 @@ const startFlushOutlineInterval = (ctx: CanvasRenderingContext2D) => {
417445 } ) ;
418446 } , 30 ) ;
419447} ;
448+
449+ const updateScheduledOutlines = ( fiber : Fiber , renders : Array < Render > ) => {
450+ for ( let i = 0 , len = renders . length ; i < len ; i ++ ) {
451+ const render = renders [ i ] ;
452+ const domFiber = getNearestHostFiber ( fiber ) ;
453+ if ( ! domFiber || ! domFiber . stateNode ) continue ;
454+
455+ if ( ReactScanInternals . scheduledOutlines . has ( fiber ) ) {
456+ const existingOutline = ReactScanInternals . scheduledOutlines . get ( fiber ) ! ;
457+ aggregateRender ( render , existingOutline . aggregatedRender ) ;
458+ } else {
459+ ReactScanInternals . scheduledOutlines . set ( fiber , {
460+ domNode : domFiber . stateNode ,
461+ aggregatedRender : {
462+ computedCurrent : null ,
463+ name :
464+ renders . find ( ( render ) => render . componentName ) ?. componentName ??
465+ 'Unknown' ,
466+ aggregatedCount : 1 ,
467+ changes : aggregateChanges ( render . changes ) ,
468+ didCommit : render . didCommit ,
469+ forget : render . forget ,
470+ fps : render . fps ,
471+ phase : new Set ( [ render . phase ] ) ,
472+ time : render . time ,
473+ unnecessary : render . unnecessary ,
474+ frame : 0 ,
475+ computedKey : null ,
476+ } ,
477+ alpha : null ,
478+ groupedAggregatedRender : null ,
479+ target : null ,
480+ current : null ,
481+ totalFrames : null ,
482+ estimatedTextWidth : null ,
483+ } ) ;
484+ }
485+ }
486+ } ;
487+ export let isProduction = false ;
420488export const start = ( ) => {
421489 if ( typeof window === 'undefined' ) return ;
422490
@@ -448,7 +516,6 @@ export const start = () => {
448516 onActive ( ) {
449517 if ( ! Store . monitor . value ) {
450518 const rdtHook = getRDTHook ( ) ;
451- let isProduction = false ;
452519 for ( const renderer of rdtHook . renderers . values ( ) ) {
453520 const buildType = detectReactBuildType ( renderer ) ;
454521 if ( buildType === 'production' ) {
@@ -548,6 +615,7 @@ export const start = () => {
548615 } ,
549616 isValidFiber,
550617 onRender ( fiber , renders ) {
618+ // todo: don't track renders at all if paused, reduce overhead
551619 if (
552620 Boolean ( ReactScanInternals . instrumentation ?. isPaused . value ) ||
553621 ! ctx ||
@@ -578,43 +646,9 @@ export const start = () => {
578646
579647 ReactScanInternals . options . value . onRender ?.( fiber , renders ) ;
580648
649+ updateScheduledOutlines ( fiber , renders ) ;
581650 for ( let i = 0 , len = renders . length ; i < len ; i ++ ) {
582651 const render = renders [ i ] ;
583- const domFiber = getNearestHostFiber ( fiber ) ;
584- if ( ! domFiber || ! domFiber . stateNode ) continue ;
585-
586- if ( ReactScanInternals . scheduledOutlines . has ( fiber ) ) {
587- const existingOutline =
588- ReactScanInternals . scheduledOutlines . get ( fiber ) ! ;
589- aggregateRender ( render , existingOutline . aggregatedRender ) ;
590- } else {
591- ReactScanInternals . scheduledOutlines . set ( fiber , {
592- domNode : domFiber . stateNode ,
593- aggregatedRender : {
594- name :
595- renders . find ( ( render ) => render . componentName ) ?. componentName ??
596- 'Unknown' ,
597- aggregatedCount : 1 ,
598- changes : aggregateChanges ( render . changes ) ,
599- didCommit : render . didCommit ,
600- forget : render . forget ,
601- fps : render . fps ,
602- phase : new Set ( [ render . phase ] ) ,
603- time : render . time ,
604- // todo: add back a when clear use case in the UI is needed for isRenderUnnecessary, or performance is optimized
605- // unnecessary: isRenderUnnecessary(fiber),
606- unnecessary : false ,
607- frame : 0 ,
608-
609- computedKey : null ,
610- } ,
611- alpha : null ,
612- groupedAggregatedRender : null ,
613- rect : null ,
614- totalFrames : null ,
615- estimatedTextWidth : null ,
616- } ) ;
617- }
618652
619653 // - audio context can take up an insane amount of cpu, todo: figure out why
620654 // - we may want to take this out of hot path
0 commit comments