From 0849ebfbf340bd28f19ceb39efeb22ea399da993 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 31 Mar 2025 10:26:04 +0200 Subject: [PATCH] perf(cdk/drag-drop): fix performance regression when destroying items #30514 changed the logic that syncs destroyed items to apply to non-dragged items as well. This led to a performance regression where swapping out a large list of items can lock up the entire browser. The problem is that the items need to be re-sorted each time an item is destroyed which is expensive. These changes resolve the issue by keeping track of the last set of items and dropping the item from it without re-sorting. Fixes #30737. --- src/cdk/drag-drop/directives/drop-list.ts | 24 ++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/cdk/drag-drop/directives/drop-list.ts b/src/cdk/drag-drop/directives/drop-list.ts index 40fa7955189d..ce1008009475 100644 --- a/src/cdk/drag-drop/directives/drop-list.ts +++ b/src/cdk/drag-drop/directives/drop-list.ts @@ -59,6 +59,9 @@ export class CdkDropList implements OnDestroy { skipSelf: true, }); + /** Refs that have been synced with the drop ref most recently. */ + private _latestSortedRefs: DragRef[] | undefined; + /** Emits when the list has been destroyed. */ private readonly _destroyed = new Subject(); @@ -222,7 +225,7 @@ export class CdkDropList implements OnDestroy { // Only sync the items while dragging since this method is // called when items are being initialized one-by-one. if (this._dropListRef.isDragging()) { - this._syncItemsWithRef(); + this._syncItemsWithRef(this.getSortedItems().map(item => item._dragRef)); } } @@ -231,7 +234,16 @@ export class CdkDropList implements OnDestroy { this._unsortedItems.delete(item); // This method might be called on destroy so we always want to sync with the ref. - this._syncItemsWithRef(); + // Note that we reuse the last set of synced items, rather than re-sorting the whole + // list, because it can slow down re-renders of large lists (see #30737). + if (this._latestSortedRefs) { + const index = this._latestSortedRefs.indexOf(item._dragRef); + + if (index > -1) { + this._latestSortedRefs.splice(index, 1); + this._syncItemsWithRef(this._latestSortedRefs); + } + } } /** Gets the registered items in the list, sorted by their position in the DOM. */ @@ -259,6 +271,7 @@ export class CdkDropList implements OnDestroy { this._group._items.delete(this); } + this._latestSortedRefs = undefined; this._unsortedItems.clear(); this._dropListRef.dispose(); this._destroyed.next(); @@ -335,7 +348,7 @@ export class CdkDropList implements OnDestroy { /** Handles events from the underlying DropListRef. */ private _handleEvents(ref: DropListRef) { ref.beforeStarted.subscribe(() => { - this._syncItemsWithRef(); + this._syncItemsWithRef(this.getSortedItems().map(item => item._dragRef)); this._changeDetectorRef.markForCheck(); }); @@ -403,7 +416,8 @@ export class CdkDropList implements OnDestroy { } /** Syncs up the registered drag items with underlying drop list ref. */ - private _syncItemsWithRef() { - this._dropListRef.withItems(this.getSortedItems().map(item => item._dragRef)); + private _syncItemsWithRef(items: DragRef[]) { + this._latestSortedRefs = items; + this._dropListRef.withItems(items); } }