@@ -312,13 +312,19 @@ const KeepAlive = ({ id, active = false, children, persistOnUnmount = false, cac
312312 }
313313
314314 setContainerNode ( containerRef . current )
315+ if ( process . env . NODE_ENV === 'development' ) {
316+ try {
317+
318+ console . debug ( '[KeepAlive] setContainerNode' , id , containerRef . current )
319+ } catch ( e ) { }
320+ }
315321 // no cleanup here; mount/unmount handled elsewhere
316322 } , [ id ] )
317323
318324 // Scroll restoration logic
319325 useEffect ( ( ) => {
320- const container = containerRef . current
321- if ( ! container ) return
326+ const target = ActivityComponent ? placeholderRef . current : containerRef . current
327+ if ( ! target ) return
322328
323329 const onScroll = ( e ) => {
324330 if ( ! active ) return
@@ -329,13 +335,13 @@ const KeepAlive = ({ id, active = false, children, persistOnUnmount = false, cac
329335 }
330336
331337 // Capture scroll events to record positions
332- container . addEventListener ( 'scroll' , onScroll , {
338+ target . addEventListener ( 'scroll' , onScroll , {
333339 capture : true ,
334340 passive : true ,
335341 } )
336342
337343 return ( ) => {
338- container . removeEventListener ( 'scroll' , onScroll , { capture : true } )
344+ target . removeEventListener ( 'scroll' , onScroll , { capture : true } )
339345 }
340346 } , [ active ] )
341347
@@ -370,40 +376,161 @@ const KeepAlive = ({ id, active = false, children, persistOnUnmount = false, cac
370376
371377 if ( ! container || ! placeholder ) return
372378
379+ // If React Activity API is available, avoid moving DOM to prevent forced reflows.
380+ // Render children inline into the placeholder and point container refs to placeholder.
381+ if ( ActivityComponent ) {
382+ try {
383+ containerRef . current = placeholder
384+ setContainerNode ( placeholder )
385+ } catch ( e ) { }
386+ return
387+ }
388+
373389 // 如果 container 还在隐藏容器里(比如从缓存恢复),把它移到当前占位符下
374390 if ( container . parentNode !== placeholder ) {
375- // 在移动DOM之前,发送自定义事件通知子组件
391+ // 在移动 DOM 之前,发送自定义事件通知子组件,然后同步移动到占位符下。
392+ // 之前使用短延迟的异步移动会导致渲染滞后和大量定时器/帧回调,移到同步移动以提升响应性。
376393 const event = new CustomEvent ( 'keepalive-dom-move' , {
377394 detail : { from : container . parentNode , to : placeholder } ,
378395 } )
379- container . dispatchEvent ( event )
380396
381- // 短暂延迟确保子组件有机会处理事件
382- setTimeout ( ( ) => {
397+ let dispatchError = null
398+ try {
399+ container . dispatchEvent ( event )
400+ } catch ( e ) {
401+ dispatchError = String ( e && e . message )
402+ }
403+
404+ if ( process . env . NODE_ENV === 'development' ) {
405+ try {
406+
407+ console . debug ( '[KeepAlive] appending container to placeholder' , id , {
408+ from : container . parentNode ,
409+ to : placeholder ,
410+ dispatchError,
411+ } )
412+ } catch ( e ) { }
413+ }
414+
415+ let appended = false
416+ try {
383417 if ( container . parentNode !== placeholder ) {
418+ container . dataset . keepaliveAttached = String ( Date . now ( ) )
384419 placeholder . appendChild ( container )
420+ appended = true
421+
422+ // Watchdog: if children not mounted soon after append, record and mark on body
423+ try {
424+ setTimeout ( ( ) => {
425+ try {
426+ if ( container . childElementCount === 0 ) {
427+ if ( typeof window !== 'undefined' ) {
428+ window . __keepalive_debug_details = window . __keepalive_debug_details || [ ]
429+ window . __keepalive_debug_details . push ( {
430+ id,
431+ note : 'no-children-after-append' ,
432+ childElementCount : container . childElementCount ,
433+ time : Date . now ( ) ,
434+ } )
435+ try {
436+ document . body . dataset . keepaliveIssue = 'no-children'
437+ } catch ( e ) { }
438+ }
439+ }
440+ } catch ( e ) { }
441+ } , 50 )
442+ } catch ( e ) { }
385443 }
386- } , 0 )
387- }
444+ } catch ( e ) {
445+ // swallow; we'll record below
446+ }
388447
389- // 如果使用 Activity,我们让 Activity 控制可见性,但我们需要确保 container 本身是 block
390- // 并且我们仍然需要手动恢复滚动位置,因为 container 是我们手动管理的 DOM
391- if ( ActivityComponent ) {
392- container . style . display = 'block'
448+ // 记录附加信息,包含父节点信息与子元素计数,便于生产环境排查
449+ try {
450+ if ( typeof window !== 'undefined' ) {
451+ window . __keepalive_debug_details = window . __keepalive_debug_details || [ ]
452+ window . __keepalive_debug_details . push ( {
453+ id,
454+ parentTag : container . parentNode ? container . parentNode . tagName : null ,
455+ parentClass : container . parentNode ? container . parentNode . className : null ,
456+ childElementCount : container . childElementCount ,
457+ appended,
458+ dispatchError,
459+ time : Date . now ( ) ,
460+ } )
461+ }
462+ } catch ( e ) { }
463+
464+ // If we just appended and this instance is active, clear any stray inline "display: none" styles
465+ if ( appended && active && shouldRender ) {
466+ try {
467+ // run in next frame to let other sync updates finish
468+ requestAnimationFrame ( ( ) => {
469+ try {
470+ const cleared = [ ]
471+ // Limit to direct descendants first for safety
472+ const nodes = Array . from ( container . querySelectorAll ( '*' ) )
473+ nodes . forEach ( ( el ) => {
474+ try {
475+ if ( el && el . style && el . style . display === 'none' ) {
476+ el . style . removeProperty ( 'display' )
477+ cleared . push ( el . tagName )
478+ }
479+ } catch ( e ) { }
480+ } )
481+
482+ // Ensure the root keepalive node is visible (force with !important)
483+ try {
484+ const root = container . querySelector ( `[data-keepalive-id="${ id } "]` )
485+ if ( root && root . style ) {
486+ root . style . setProperty ( 'display' , 'block' , 'important' )
487+ }
488+ } catch ( e ) { }
489+
490+ if ( typeof window !== 'undefined' ) {
491+ window . __keepalive_debug_details = window . __keepalive_debug_details || [ ]
492+ window . __keepalive_debug_details . push ( {
493+ id,
494+ note : 'cleared-inline-display-none' ,
495+ clearedCount : cleared . length ,
496+ clearedTagsSample : cleared . slice ( 0 , 5 ) ,
497+ time : Date . now ( ) ,
498+ } )
499+ }
500+ } catch ( e ) { }
501+ } )
502+ } catch ( e ) { }
503+ }
504+
505+ // Ensure root visible when active (force with !important) — covers cases where append didn't run
393506 if ( active && shouldRender ) {
394- scrollPos . current . forEach ( ( pos , node ) => {
395- if ( node . isConnected ) {
396- node . scrollLeft = pos . left
397- node . scrollTop = pos . top
507+ try {
508+ const root = container . querySelector ( `[data-keepalive-id="${ id } "]` )
509+ if ( root && root . style ) {
510+ root . style . setProperty ( 'display' , 'block' , 'important' )
511+ // also remove any stray inline display:none on descendants
512+ try {
513+ const nodes = Array . from ( container . querySelectorAll ( '*' ) )
514+ nodes . forEach ( ( el ) => {
515+ try {
516+ if ( el && el . style && el . style . display === 'none' ) {
517+ el . style . removeProperty ( 'display' )
518+ }
519+ } catch ( e ) { }
520+ } )
521+ } catch ( e ) { }
522+
523+ if ( typeof window !== 'undefined' ) {
524+ window . __keepalive_debug_details = window . __keepalive_debug_details || [ ]
525+ window . __keepalive_debug_details . push ( {
526+ id,
527+ note : 'force-visible-root' ,
528+ time : Date . now ( ) ,
529+ } )
530+ }
398531 }
399- } )
532+ } catch ( e ) { }
400533 }
401- return
402- }
403-
404- if ( active && shouldRender ) {
405- // 使用 CSS 切换可见性,性能远高于 appendChild
406- container . style . display = 'block'
407534
408535 // Restore scroll positions
409536 scrollPos . current . forEach ( ( pos , node ) => {
@@ -418,14 +545,58 @@ const KeepAlive = ({ id, active = false, children, persistOnUnmount = false, cac
418545 }
419546 } , [ active , shouldRender ] )
420547
548+ // Record lightweight runtime debug info in an effect (avoid doing impure work during render)
549+ useEffect ( ( ) => {
550+ if ( typeof window === 'undefined' ) return
551+ if ( ! shouldRender ) return
552+ try {
553+ window . __keepalive_debug = window . __keepalive_debug || [ ]
554+ window . __keepalive_debug . push ( {
555+ id,
556+ active,
557+ shouldRender,
558+ containerNode : ! ! containerNode ,
559+ childrenType : typeof children ,
560+ time : Date . now ( ) ,
561+ } )
562+ } catch ( e ) { }
563+ } , [ shouldRender , active , containerNode , id , children ] )
564+
565+ // Activity mode: render children inline and avoid DOM moving to reduce reflows
566+ useEffect ( ( ) => {
567+ if ( ! ActivityComponent ) return
568+ if ( ! active || ! shouldRender ) return
569+ try {
570+ requestAnimationFrame ( ( ) => {
571+ try {
572+ const p = placeholderRef . current
573+ if ( ! p ) return
574+ const nodes = Array . from ( p . querySelectorAll ( '*' ) )
575+ nodes . forEach ( ( el ) => {
576+ try {
577+ if ( el && el . style && el . style . display === 'none' ) {
578+ el . style . removeProperty ( 'display' )
579+ }
580+ } catch ( e ) { }
581+ } )
582+ if ( typeof window !== 'undefined' ) {
583+ window . __keepalive_debug_details = window . __keepalive_debug_details || [ ]
584+ window . __keepalive_debug_details . push ( { id, note : 'activity-mode-cleaned-display' , time : Date . now ( ) } )
585+ }
586+ } catch ( e ) { }
587+ } )
588+ } catch ( e ) { }
589+ } , [ ActivityComponent , active , isActivityVisible , shouldRender ] )
590+
421591 if ( ! shouldRender ) return null
422592
423593 if ( ActivityComponent ) {
424594 return (
425595 < KeepAliveContext . Provider value = { active } >
426596 < ActivityComponent mode = { isActivityVisible ? 'visible' : 'hidden' } >
427- < div ref = { placeholderRef } style = { { width : '100%' , height : '100%' } } />
428- { containerNode && createPortal ( children , containerNode ) }
597+ < div ref = { placeholderRef } style = { { width : '100%' , height : '100%' } } >
598+ { children }
599+ </ div >
429600 </ ActivityComponent >
430601 </ KeepAliveContext . Provider >
431602 )
0 commit comments