@@ -28,10 +28,7 @@ import {
28
28
} from 'src/core/utils' ;
29
29
import { readLocalStorage , saveLocalStorage } from '@web-utils/helpers' ;
30
30
import { initReactScanOverlay } from './web/overlay' ;
31
- import {
32
- createInstrumentation ,
33
- type Render ,
34
- } from './instrumentation' ;
31
+ import { createInstrumentation , type Render } from './instrumentation' ;
35
32
import { createToolbar } from './web/toolbar' ;
36
33
import type { InternalInteraction } from './monitor/types' ;
37
34
import { type getSession } from './monitor/utils' ;
@@ -133,6 +130,24 @@ export interface Options {
133
130
*/
134
131
animationSpeed ?: 'slow' | 'fast' | 'off' ;
135
132
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
+
136
151
onCommitStart ?: ( ) => void ;
137
152
onRender ?: ( fiber : Fiber , renders : Array < Render > ) => void ;
138
153
onCommitFinish ?: ( ) => void ;
@@ -217,6 +232,8 @@ export const ReactScanInternals: Internals = {
217
232
alwaysShowLabels : false ,
218
233
animationSpeed : 'fast' ,
219
234
dangerouslyForceRunInProduction : false ,
235
+ smoothlyAnimateOutlines : true ,
236
+ trackUnnecessaryRenders : true ,
220
237
} ) ,
221
238
onRender : null ,
222
239
scheduledOutlines : new Map ( ) ,
@@ -281,6 +298,17 @@ const validateOptions = (options: Partial<Options>): Partial<Options> => {
281
298
( validOptions as any ) [ key ] = value ;
282
299
}
283
300
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
+ }
284
312
default :
285
313
errors . push ( `- Unknown option "${ key } "` ) ;
286
314
}
@@ -417,6 +445,46 @@ const startFlushOutlineInterval = (ctx: CanvasRenderingContext2D) => {
417
445
} ) ;
418
446
} , 30 ) ;
419
447
} ;
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 ;
420
488
export const start = ( ) => {
421
489
if ( typeof window === 'undefined' ) return ;
422
490
@@ -448,7 +516,6 @@ export const start = () => {
448
516
onActive ( ) {
449
517
if ( ! Store . monitor . value ) {
450
518
const rdtHook = getRDTHook ( ) ;
451
- let isProduction = false ;
452
519
for ( const renderer of rdtHook . renderers . values ( ) ) {
453
520
const buildType = detectReactBuildType ( renderer ) ;
454
521
if ( buildType === 'production' ) {
@@ -548,6 +615,7 @@ export const start = () => {
548
615
} ,
549
616
isValidFiber,
550
617
onRender ( fiber , renders ) {
618
+ // todo: don't track renders at all if paused, reduce overhead
551
619
if (
552
620
Boolean ( ReactScanInternals . instrumentation ?. isPaused . value ) ||
553
621
! ctx ||
@@ -578,43 +646,9 @@ export const start = () => {
578
646
579
647
ReactScanInternals . options . value . onRender ?.( fiber , renders ) ;
580
648
649
+ updateScheduledOutlines ( fiber , renders ) ;
581
650
for ( let i = 0 , len = renders . length ; i < len ; i ++ ) {
582
651
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
- }
618
652
619
653
// - audio context can take up an insane amount of cpu, todo: figure out why
620
654
// - we may want to take this out of hot path
0 commit comments