@@ -3,8 +3,6 @@ import type {ComponentProps} from '../utils/types'
33import { StressTest } from '../utils/StressTest'
44import { TableIcon } from '@primer/octicons-react'
55import { ActionList } from '.'
6- import React , { useState , useEffect , useRef , useCallback , memo } from 'react'
7- import afterFrame from 'afterframe'
86
97export default {
108 title : 'StressTests/Components/ActionList' ,
@@ -50,140 +48,3 @@ export const SingleSelect = () => {
5048 />
5149 )
5250}
53-
54- export const ParentRerender = ( ) => {
55- return < ContextMemoizationBenchmark />
56- }
57-
58- /**
59- * This benchmark isolates the effect of context value memoization.
60- *
61- * Setup: A parent component holds a counter that increments 100 times.
62- * The ActionList is rendered via a memoized child component (MemoizedList),
63- * so React only re-renders it if props or context change.
64- *
65- * - Without useMemo on ListContext/ItemContext: the context values are new
66- * objects every render, so React re-renders all context consumers (every
67- * Description, LeadingVisual, TrailingVisual, Selection in every Item).
68- * - With useMemo: context values are referentially stable, React bails out
69- * of re-rendering the consumers entirely.
70- */
71-
72- const listItems = Array . from ( { length : 100 } , ( _ , i ) => ( {
73- name : `Project ${ i + 1 } ` ,
74- scope : `Scope ${ i + 1 } ` ,
75- } ) )
76-
77- // Memoized list component: only re-renders when props change or context forces it
78- const MemoizedList = memo ( function MemoizedList ( ) {
79- return (
80- < ActionList selectionVariant = "single" showDividers role = "menu" aria-label = "Project" >
81- { listItems . map ( ( project , index ) => (
82- < ActionList . Item key = { index } role = "menuitemradio" selected = { index === 0 } aria-checked = { index === 0 } >
83- < ActionList . LeadingVisual >
84- < TableIcon />
85- </ ActionList . LeadingVisual >
86- { project . name }
87- < ActionList . Description variant = "block" > { project . scope } </ ActionList . Description >
88- </ ActionList . Item >
89- ) ) }
90- </ ActionList >
91- )
92- } )
93-
94- function ContextMemoizationBenchmark ( ) {
95- const totalIterations = 100
96- const [ count , setCount ] = useState ( 0 )
97- const [ running , setRunning ] = useState ( false )
98- const [ results , setResults ] = useState < { median : number ; average : number ; min : number ; max : number } | null > ( null )
99- const observerRef = useRef < { observer : PerformanceObserver ; data : number [ ] } | null > ( null )
100-
101- useEffect ( ( ) => {
102- const duration : number [ ] = [ ]
103- const obs = new PerformanceObserver ( list => {
104- for ( const entry of list . getEntries ( ) ) {
105- if ( entry . entryType === 'measure' && entry . name === 'ctx-rerender' ) {
106- duration . push ( entry . duration )
107- }
108- }
109- } )
110- obs . observe ( { entryTypes : [ 'measure' ] } )
111- observerRef . current = { data : duration , observer : obs }
112- return ( ) => obs . disconnect ( )
113- } , [ ] )
114-
115- const start = useCallback ( ( ) => {
116- if ( observerRef . current ) observerRef . current . data . length = 0
117- setResults ( null )
118- setRunning ( true )
119- setCount ( 0 )
120-
121- let i = 0
122- const interval = setInterval ( ( ) => {
123- if ( i < totalIterations - 1 ) {
124- performance . mark ( 'ctx-start' )
125- setCount ( c => c + 1 )
126- afterFrame ( ( ) => {
127- performance . mark ( 'ctx-end' )
128- performance . measure ( 'ctx-rerender' , 'ctx-start' , 'ctx-end' )
129- } )
130- i ++
131- } else {
132- clearInterval ( interval )
133- setTimeout ( ( ) => {
134- const durations = observerRef . current ?. data ?? [ ]
135- const sorted = [ ...durations ] . sort ( ( a , b ) => a - b )
136- setResults ( {
137- median : sorted [ Math . floor ( sorted . length / 2 ) ] ?? 0 ,
138- average : durations . reduce ( ( a , b ) => a + b , 0 ) / durations . length ,
139- min : Math . min ( ...durations ) ,
140- max : Math . max ( ...durations ) ,
141- } )
142- setRunning ( false )
143- } , 100 )
144- }
145- } , 10 )
146- } , [ ] )
147-
148- return (
149- < div style = { { padding : 16 } } >
150- < div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , marginBottom : 16 } } >
151- < div >
152- < strong > ActionList Context Memoization</ strong >
153- < p style = { { color : '#656d76' , margin : '4px 0' } } >
154- Parent re-renders 100x with a counter. ActionList is memoized, so only context changes trigger item
155- re-renders. Tests whether ListContext/ItemContext values are stable.
156- </ p >
157- </ div >
158- < button
159- type = "button"
160- onClick = { start }
161- disabled = { running }
162- style = { {
163- padding : '8px 16px' ,
164- background : running ? '#ccc' : '#1a7f37' ,
165- color : 'white' ,
166- border : 'none' ,
167- borderRadius : 6 ,
168- cursor : running ? 'default' : 'pointer' ,
169- } }
170- >
171- { running ? `Running ${ count } /${ totalIterations } ...` : results ? 'Re-run' : 'Start' }
172- </ button >
173- </ div >
174-
175- { /* The counter forces this component to re-render, but MemoizedList should bail out if context is stable */ }
176- < div style = { { display : 'none' } } > Counter: { count } </ div >
177- < MemoizedList />
178-
179- { results && (
180- < div style = { { marginTop : 16 , fontFamily : 'monospace' , fontSize : 14 } } >
181- < strong > Median: { results . median . toFixed ( 2 ) } ms</ strong >
182- { ' | ' } Average: { results . average . toFixed ( 2 ) } ms{ ' | ' }
183- Min: { results . min . toFixed ( 2 ) } ms{ ' | ' }
184- Max: { results . max . toFixed ( 2 ) } ms
185- </ div >
186- ) }
187- </ div >
188- )
189- }
0 commit comments