@@ -361,7 +361,11 @@ export class Virtualizer<
361361 isScrolling = false
362362 measurementsCache : Array < VirtualItem > = [ ]
363363 private itemSizeCache = new Map < Key , number > ( )
364+ private laneAssignments = new Map < number , number > ( ) // index → lane cache
364365 private pendingMeasuredCacheIndexes : Array < number > = [ ]
366+ private prevLanes : number | undefined = undefined
367+ private lanesChangedFlag = false
368+ private lanesSettling = false
365369 scrollRect : Rect | null = null
366370 scrollOffset : number | null = null
367371 scrollDirection : ScrollDirection | null = null
@@ -610,22 +614,47 @@ export class Virtualizer<
610614 : undefined
611615 }
612616
617+ // Helper to find the last item in a specific lane before currentIndex
618+ private findLastItemInLane = (
619+ measurements : Array < VirtualItem > ,
620+ currentIndex : number ,
621+ lane : number ,
622+ ) : VirtualItem | undefined => {
623+ for ( let m = currentIndex - 1 ; m >= 0 ; m -- ) {
624+ if ( measurements [ m ] ?. lane === lane ) {
625+ return measurements [ m ]
626+ }
627+ }
628+ return undefined
629+ }
630+
613631 private getMeasurementOptions = memo (
614632 ( ) => [
615633 this . options . count ,
616634 this . options . paddingStart ,
617635 this . options . scrollMargin ,
618636 this . options . getItemKey ,
619637 this . options . enabled ,
638+ this . options . lanes ,
620639 ] ,
621- ( count , paddingStart , scrollMargin , getItemKey , enabled ) => {
640+ ( count , paddingStart , scrollMargin , getItemKey , enabled , lanes ) => {
641+ const lanesChanged = this . prevLanes !== undefined && this . prevLanes !== lanes
642+
643+ if ( lanesChanged ) {
644+ // Set flag for getMeasurements to handle
645+ this . lanesChangedFlag = true
646+ }
647+
648+ this . prevLanes = lanes
622649 this . pendingMeasuredCacheIndexes = [ ]
650+
623651 return {
624652 count,
625653 paddingStart,
626654 scrollMargin,
627655 getItemKey,
628656 enabled,
657+ lanes,
629658 }
630659 } ,
631660 {
@@ -636,41 +665,93 @@ export class Virtualizer<
636665 private getMeasurements = memo (
637666 ( ) => [ this . getMeasurementOptions ( ) , this . itemSizeCache ] ,
638667 (
639- { count, paddingStart, scrollMargin, getItemKey, enabled } ,
668+ { count, paddingStart, scrollMargin, getItemKey, enabled, lanes } ,
640669 itemSizeCache ,
641670 ) => {
642671 if ( ! enabled ) {
643672 this . measurementsCache = [ ]
644673 this . itemSizeCache . clear ( )
674+ this . laneAssignments . clear ( )
645675 return [ ]
646676 }
647677
678+ // Clean up stale lane cache entries when count decreases
679+ if ( this . laneAssignments . size > count ) {
680+ for ( const index of this . laneAssignments . keys ( ) ) {
681+ if ( index >= count ) {
682+ this . laneAssignments . delete ( index )
683+ }
684+ }
685+ }
686+
687+ // ✅ Force complete recalculation when lanes change
688+ if ( this . lanesChangedFlag ) {
689+ this . lanesChangedFlag = false // Reset immediately
690+ this . lanesSettling = true // Start settling period
691+ this . measurementsCache = [ ]
692+ this . itemSizeCache . clear ( )
693+ this . laneAssignments . clear ( ) // Clear lane cache for new lane count
694+ // Clear pending indexes to force min = 0
695+ this . pendingMeasuredCacheIndexes = [ ]
696+ }
697+
648698 if ( this . measurementsCache . length === 0 ) {
649699 this . measurementsCache = this . options . initialMeasurementsCache
650700 this . measurementsCache . forEach ( ( item ) => {
651701 this . itemSizeCache . set ( item . key , item . size )
652702 } )
653703 }
654704
655- const min =
705+ // ✅ During lanes settling, ignore pendingMeasuredCacheIndexes to prevent repositioning
706+ const min = this . lanesSettling ? 0 : (
656707 this . pendingMeasuredCacheIndexes . length > 0
657708 ? Math . min ( ...this . pendingMeasuredCacheIndexes )
658709 : 0
710+ )
659711 this . pendingMeasuredCacheIndexes = [ ]
660712
713+ // ✅ End settling period when cache is fully built
714+ if ( this . lanesSettling && this . measurementsCache . length === count ) {
715+ this . lanesSettling = false
716+ }
717+
661718 const measurements = this . measurementsCache . slice ( 0 , min )
662719
663720 for ( let i = min ; i < count ; i ++ ) {
664721 const key = getItemKey ( i )
665722
666- const furthestMeasurement =
667- this . options . lanes === 1
668- ? measurements [ i - 1 ]
669- : this . getFurthestMeasurement ( measurements , i )
670-
671- const start = furthestMeasurement
672- ? furthestMeasurement . end + this . options . gap
673- : paddingStart + scrollMargin
723+ // Check for cached lane assignment
724+ const cachedLane = this . laneAssignments . get ( i )
725+ let lane : number
726+ let start : number
727+
728+ if ( cachedLane !== undefined && this . options . lanes > 1 ) {
729+ // Use cached lane - find previous item in same lane for start position
730+ lane = cachedLane
731+ const prevInLane = this . findLastItemInLane ( measurements , i , lane )
732+ start = prevInLane
733+ ? prevInLane . end + this . options . gap
734+ : paddingStart + scrollMargin
735+ } else {
736+ // No cache - use original logic (find shortest lane)
737+ const furthestMeasurement =
738+ this . options . lanes === 1
739+ ? measurements [ i - 1 ]
740+ : this . getFurthestMeasurement ( measurements , i )
741+
742+ start = furthestMeasurement
743+ ? furthestMeasurement . end + this . options . gap
744+ : paddingStart + scrollMargin
745+
746+ lane = furthestMeasurement
747+ ? furthestMeasurement . lane
748+ : i % this . options . lanes
749+
750+ // Cache the lane assignment
751+ if ( this . options . lanes > 1 ) {
752+ this . laneAssignments . set ( i , lane )
753+ }
754+ }
674755
675756 const measuredSize = itemSizeCache . get ( key )
676757 const size =
@@ -680,10 +761,6 @@ export class Virtualizer<
680761
681762 const end = start + size
682763
683- const lane = furthestMeasurement
684- ? furthestMeasurement . lane
685- : i % this . options . lanes
686-
687764 measurements [ i ] = {
688765 index : i ,
689766 start,
@@ -1077,6 +1154,7 @@ export class Virtualizer<
10771154
10781155 measure = ( ) => {
10791156 this . itemSizeCache = new Map ( )
1157+ this . laneAssignments = new Map ( ) // Clear lane cache for full re-layout
10801158 this . notify ( false )
10811159 }
10821160}
0 commit comments