@@ -234,16 +234,11 @@ export function VirtualElement({
234234 )
235235
236236 const isScrolling = ( ) : boolean => {
237- // Don't do updates while scrolling:
238- if ( getViewPortScrollingState ( ) . isProgrammaticScrollInProgress ) {
239- return true
240- }
241- // And wait if a programmatic scroll was done recently:
242- const timeSinceLastProgrammaticScroll = Date . now ( ) - getViewPortScrollingState ( ) . lastProgrammaticScrollTime
243- if ( timeSinceLastProgrammaticScroll < 100 ) {
244- return true
245- }
246- return false
237+ const { isProgrammaticScrollInProgress, lastProgrammaticScrollTime } = getViewPortScrollingState ( )
238+ if ( ! isProgrammaticScrollInProgress ) return false
239+
240+ // Safety valve: stale programmatic scroll state should not block virtualization indefinitely.
241+ return Date . now ( ) - lastProgrammaticScrollTime < 1200
247242 }
248243
249244 useEffect ( ( ) => {
@@ -423,7 +418,7 @@ export class ElementObserverManager {
423418 private resizeObserver : ResizeObserver
424419 private mutationObserver : MutationObserver
425420 private observedElements : Map < HTMLElement , ( ) => void >
426- private pendingReconnectFrame : number | undefined
421+ private isMutationObserverActive = false
427422
428423 private pruneDetachedObservedElements ( ) : void {
429424 for ( const observedElement of Array . from ( this . observedElements . keys ( ) ) ) {
@@ -448,34 +443,59 @@ export class ElementObserverManager {
448443 } )
449444 } )
450445
451- // Configure MutationObserver
446+ // Configure MutationObserver once and only connect/disconnect based on active observed elements.
452447 this . mutationObserver = new MutationObserver ( ( mutations ) => {
448+ if ( this . observedElements . size === 0 ) return
449+
453450 this . pruneDetachedObservedElements ( )
454451 const targets = new Set < HTMLElement > ( )
455452
456453 mutations . forEach ( ( mutation ) => {
457- const target = mutation . target as HTMLElement
458- if ( ! document . contains ( target ) ) return
459- // Find the closest observed element
460- let element = target
454+ let element : HTMLElement | null = null
455+ if ( mutation . target instanceof HTMLElement ) {
456+ element = mutation . target
457+ } else {
458+ element = mutation . target . parentElement
459+ }
460+
461+ if ( ! element || ! document . contains ( element ) ) return
462+
461463 while ( element ) {
462464 if ( this . observedElements . has ( element ) ) {
463465 targets . add ( element )
464466 break
465467 }
466- if ( ! element . parentElement ) break
467468 element = element . parentElement
468469 }
469470 } )
470471
471- // Call callbacks for affected elements
472472 targets . forEach ( ( element ) => {
473473 const callback = this . observedElements . get ( element )
474474 if ( callback ) callback ( )
475475 } )
476476 } )
477477 }
478478
479+ private ensureMutationObserverConnected ( ) : void {
480+ if ( this . isMutationObserverActive ) return
481+ if ( this . observedElements . size === 0 ) return
482+ if ( ! document . body ) return
483+
484+ this . mutationObserver . observe ( document . body , {
485+ childList : true ,
486+ subtree : true ,
487+ attributes : true ,
488+ characterData : true ,
489+ } )
490+ this . isMutationObserverActive = true
491+ }
492+
493+ private disconnectMutationObserver ( ) : void {
494+ if ( ! this . isMutationObserverActive ) return
495+ this . mutationObserver . disconnect ( )
496+ this . isMutationObserverActive = false
497+ }
498+
479499 public static getInstance ( ) : ElementObserverManager {
480500 if ( ! ElementObserverManager . instance ) {
481501 ElementObserverManager . instance = new ElementObserverManager ( )
@@ -490,55 +510,18 @@ export class ElementObserverManager {
490510
491511 this . observedElements . set ( element , callback )
492512 this . resizeObserver . observe ( element )
493- if ( ! this . pendingReconnectFrame ) {
494- this . mutationObserver . observe ( element , {
495- childList : true ,
496- subtree : true ,
497- attributes : true ,
498- characterData : true ,
499- } )
500- }
513+ this . ensureMutationObserverConnected ( )
501514 }
502515
503516 public unobserve ( element : HTMLElement ) : void {
504517 if ( ! element ) return
505518 this . observedElements . delete ( element )
506519 this . resizeObserver . unobserve ( element )
507520 this . pruneDetachedObservedElements ( )
508- this . mutationObserver . disconnect ( )
509521
510522 if ( this . observedElements . size === 0 ) {
511- if ( this . pendingReconnectFrame ) {
512- window . cancelAnimationFrame ( this . pendingReconnectFrame )
513- this . pendingReconnectFrame = undefined
514- }
515- this . mutationObserver . disconnect ( )
516523 this . resizeObserver . disconnect ( )
517- return
518- }
519-
520- if ( ! this . pendingReconnectFrame ) {
521- this . pendingReconnectFrame = window . requestAnimationFrame ( ( ) => {
522- this . pendingReconnectFrame = undefined
523-
524- // MutationObserver has no per-element unobserve, so we reconnect once per frame.
525- this . pruneDetachedObservedElements ( )
526-
527- if ( this . observedElements . size === 0 ) {
528- this . resizeObserver . disconnect ( )
529- return
530- }
531-
532- this . observedElements . forEach ( ( _ , el ) => {
533- if ( ! document . contains ( el ) ) return
534- this . mutationObserver . observe ( el , {
535- childList : true ,
536- subtree : true ,
537- attributes : true ,
538- characterData : true ,
539- } )
540- } )
541- } )
524+ this . disconnectMutationObserver ( )
542525 }
543526 }
544527}
0 commit comments