From 8e4a7ab52bc85172efd12e42304e1b8da446ff75 Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Fri, 13 Oct 2023 13:41:13 +0200 Subject: [PATCH] perf(core): avoid repeated access to LContainer and trackBy calculation (#52227) Assuming that the trackBy function is a pure derivation from the collection object and its index, we can skip trackBy calculation if items in the live and new colelction have the same identity and index. Additionally this change minimizes access to the LContainer array. PR Close #52227 --- .../core/src/render3/list_reconciliation.ts | 54 ++++++++++++++----- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/packages/core/src/render3/list_reconciliation.ts b/packages/core/src/render3/list_reconciliation.ts index a6cb347c1b9..b705ed6c72c 100644 --- a/packages/core/src/render3/list_reconciliation.ts +++ b/packages/core/src/render3/list_reconciliation.ts @@ -46,6 +46,20 @@ export abstract class LiveCollection { } } +function valuesMatching( + liveIdx: number, liveValue: V, newIdx: number, newValue: V, + trackBy: TrackByFunction): number { + if (liveIdx === newIdx && Object.is(liveValue, newValue)) { + // matching and no value identity to update + return 1; + } else if (Object.is(trackBy(liveIdx, liveValue), trackBy(newIdx, newValue))) { + // matching but requires value identity update + return -1; + } + + return 0; +} + /** * The live collection reconciliation algorithm that perform various in-place operations, so it * reflects the content of the new (incoming) collection. @@ -84,11 +98,13 @@ export function reconcile( while (liveStartIdx <= liveEndIdx && liveStartIdx <= newEndIdx) { // compare from the beginning const liveStartValue = liveCollection.at(liveStartIdx); - const liveStartKey = trackByFn(liveStartIdx, liveStartValue); const newStartValue = newCollection[liveStartIdx]; - const newStartKey = trackByFn(liveStartIdx, newStartValue); - if (Object.is(liveStartKey, newStartKey)) { - liveCollection.updateValue(liveStartIdx, newStartValue); + const isStartMatching = + valuesMatching(liveStartIdx, liveStartValue, liveStartIdx, newStartValue, trackByFn); + if (isStartMatching !== 0) { + if (isStartMatching < 0) { + liveCollection.updateValue(liveStartIdx, newStartValue); + } liveStartIdx++; continue; } @@ -96,22 +112,28 @@ export function reconcile( // compare from the end // TODO(perf): do _all_ the matching from the end const liveEndValue = liveCollection.at(liveEndIdx); - const liveEndKey = trackByFn(liveEndIdx, liveEndValue); - const newEndItem = newCollection[newEndIdx]; - const newEndKey = trackByFn(newEndIdx, newEndItem); - if (Object.is(liveEndKey, newEndKey)) { - liveCollection.updateValue(liveEndIdx, newEndItem); + const newEndValue = newCollection[newEndIdx]; + const isEndMatching = + valuesMatching(liveEndIdx, liveEndValue, newEndIdx, newEndValue, trackByFn); + if (isEndMatching !== 0) { + if (isEndMatching < 0) { + liveCollection.updateValue(liveEndIdx, newEndValue); + } liveEndIdx--; newEndIdx--; continue; } // Detect swap / moves: + const liveStartKey = trackByFn(liveStartIdx, liveStartValue); + const liveEndKey = trackByFn(liveEndIdx, liveEndValue); + const newStartKey = trackByFn(liveStartIdx, newStartValue); + const newEndKey = trackByFn(newEndIdx, newEndValue); if (Object.is(newStartKey, liveEndKey) && Object.is(newEndKey, liveStartKey)) { // swap on both ends; liveCollection.swap(liveStartIdx, liveEndIdx); liveCollection.updateValue(liveStartIdx, newStartValue); - liveCollection.updateValue(liveEndIdx, newEndItem); + liveCollection.updateValue(liveEndIdx, newEndValue); newEndIdx--; liveStartIdx++; liveEndIdx--; @@ -164,13 +186,17 @@ export function reconcile( const newCollectionIterator = newCollection[Symbol.iterator](); let newIterationResult = newCollectionIterator.next(); while (!newIterationResult.done && liveStartIdx <= liveEndIdx) { + const liveValue = liveCollection.at(liveStartIdx); const newValue = newIterationResult.value; const newKey = trackByFn(liveStartIdx, newValue); - const liveValue = liveCollection.at(liveStartIdx); const liveKey = trackByFn(liveStartIdx, liveValue); - if (Object.is(liveKey, newKey)) { - // found a match - move on - liveCollection.updateValue(liveStartIdx, newValue); + const isStartMatching = + valuesMatching(liveStartIdx, liveValue, liveStartIdx, newValue, trackByFn); + if (isStartMatching !== 0) { + // found a match - move on, but update value + if (isStartMatching < 0) { + liveCollection.updateValue(liveStartIdx, newValue); + } liveStartIdx++; newIterationResult = newCollectionIterator.next(); } else {