@@ -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
@@ -617,15 +621,27 @@ export class Virtualizer<
617621 this . options . scrollMargin ,
618622 this . options . getItemKey ,
619623 this . options . enabled ,
624+ this . options . lanes ,
620625 ] ,
621- ( count , paddingStart , scrollMargin , getItemKey , enabled ) => {
626+ ( count , paddingStart , scrollMargin , getItemKey , enabled , lanes ) => {
627+ const lanesChanged =
628+ this . prevLanes !== undefined && this . prevLanes !== lanes
629+
630+ if ( lanesChanged ) {
631+ // Set flag for getMeasurements to handle
632+ this . lanesChangedFlag = true
633+ }
634+
635+ this . prevLanes = lanes
622636 this . pendingMeasuredCacheIndexes = [ ]
637+
623638 return {
624639 count,
625640 paddingStart,
626641 scrollMargin,
627642 getItemKey,
628643 enabled,
644+ lanes,
629645 }
630646 } ,
631647 {
@@ -636,41 +652,108 @@ export class Virtualizer<
636652 private getMeasurements = memo (
637653 ( ) => [ this . getMeasurementOptions ( ) , this . itemSizeCache ] ,
638654 (
639- { count, paddingStart, scrollMargin, getItemKey, enabled } ,
655+ { count, paddingStart, scrollMargin, getItemKey, enabled, lanes } ,
640656 itemSizeCache ,
641657 ) => {
642658 if ( ! enabled ) {
643659 this . measurementsCache = [ ]
644660 this . itemSizeCache . clear ( )
661+ this . laneAssignments . clear ( )
645662 return [ ]
646663 }
647664
665+ // Clean up stale lane cache entries when count decreases
666+ if ( this . laneAssignments . size > count ) {
667+ for ( const index of this . laneAssignments . keys ( ) ) {
668+ if ( index >= count ) {
669+ this . laneAssignments . delete ( index )
670+ }
671+ }
672+ }
673+
674+ // ✅ Force complete recalculation when lanes change
675+ if ( this . lanesChangedFlag ) {
676+ this . lanesChangedFlag = false // Reset immediately
677+ this . lanesSettling = true // Start settling period
678+ this . measurementsCache = [ ]
679+ this . itemSizeCache . clear ( )
680+ this . laneAssignments . clear ( ) // Clear lane cache for new lane count
681+ // Clear pending indexes to force min = 0
682+ this . pendingMeasuredCacheIndexes = [ ]
683+ }
684+
648685 if ( this . measurementsCache . length === 0 ) {
649686 this . measurementsCache = this . options . initialMeasurementsCache
650687 this . measurementsCache . forEach ( ( item ) => {
651688 this . itemSizeCache . set ( item . key , item . size )
652689 } )
653690 }
654691
655- const min =
656- this . pendingMeasuredCacheIndexes . length > 0
692+ // ✅ During lanes settling, ignore pendingMeasuredCacheIndexes to prevent repositioning
693+ const min = this . lanesSettling
694+ ? 0
695+ : this . pendingMeasuredCacheIndexes . length > 0
657696 ? Math . min ( ...this . pendingMeasuredCacheIndexes )
658697 : 0
659698 this . pendingMeasuredCacheIndexes = [ ]
660699
700+ // ✅ End settling period when cache is fully built
701+ if ( this . lanesSettling && this . measurementsCache . length === count ) {
702+ this . lanesSettling = false
703+ }
704+
661705 const measurements = this . measurementsCache . slice ( 0 , min )
662706
707+ // ✅ Performance: Track last item index per lane for O(1) lookup
708+ const laneLastIndex : Array < number | undefined > = new Array ( lanes ) . fill (
709+ undefined ,
710+ )
711+
712+ // Initialize from existing measurements (before min)
713+ for ( let m = 0 ; m < min ; m ++ ) {
714+ const item = measurements [ m ]
715+ if ( item ) {
716+ laneLastIndex [ item . lane ] = m
717+ }
718+ }
719+
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 - O(1) lookup for previous item in same lane
730+ lane = cachedLane
731+ const prevIndex = laneLastIndex [ lane ]
732+ const prevInLane =
733+ prevIndex !== undefined ? measurements [ prevIndex ] : undefined
734+ start = prevInLane
735+ ? prevInLane . end + this . options . gap
736+ : paddingStart + scrollMargin
737+ } else {
738+ // No cache - use original logic (find shortest lane)
739+ const furthestMeasurement =
740+ this . options . lanes === 1
741+ ? measurements [ i - 1 ]
742+ : this . getFurthestMeasurement ( measurements , i )
743+
744+ start = furthestMeasurement
745+ ? furthestMeasurement . end + this . options . gap
746+ : paddingStart + scrollMargin
747+
748+ lane = furthestMeasurement
749+ ? furthestMeasurement . lane
750+ : i % this . options . lanes
751+
752+ // Cache the lane assignment
753+ if ( this . options . lanes > 1 ) {
754+ this . laneAssignments . set ( i , lane )
755+ }
756+ }
674757
675758 const measuredSize = itemSizeCache . get ( key )
676759 const size =
@@ -680,10 +763,6 @@ export class Virtualizer<
680763
681764 const end = start + size
682765
683- const lane = furthestMeasurement
684- ? furthestMeasurement . lane
685- : i % this . options . lanes
686-
687766 measurements [ i ] = {
688767 index : i ,
689768 start,
@@ -692,6 +771,9 @@ export class Virtualizer<
692771 key,
693772 lane,
694773 }
774+
775+ // ✅ Performance: Update lane's last item index
776+ laneLastIndex [ lane ] = i
695777 }
696778
697779 this . measurementsCache = measurements
@@ -1077,6 +1159,7 @@ export class Virtualizer<
10771159
10781160 measure = ( ) => {
10791161 this . itemSizeCache = new Map ( )
1162+ this . laneAssignments = new Map ( ) // Clear lane cache for full re-layout
10801163 this . notify ( false )
10811164 }
10821165}
0 commit comments