From 80b0fbba1f9e3a8ce9a89aa88ae3efd4a1f60271 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 26 Dec 2025 10:14:21 +0200 Subject: [PATCH] fix(core): avoid leaking view data in animations The animations code currently tracks which views have running leave animations by adding them to a `Set`. This can leak memory if we don't clean something up on time. These changes switch to tracking the views by their ID which doesn't risk retaining the view. Fixes #66255. --- packages/core/src/animation/longest_animation.ts | 3 ++- packages/core/src/render3/instructions/animation.ts | 6 +++--- packages/core/src/render3/instructions/control_flow.ts | 5 +++-- packages/core/src/render3/node_manipulation.ts | 7 ++++--- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/core/src/animation/longest_animation.ts b/packages/core/src/animation/longest_animation.ts index 46ced7b4f38..2ec1a520528 100644 --- a/packages/core/src/animation/longest_animation.ts +++ b/packages/core/src/animation/longest_animation.ts @@ -147,4 +147,5 @@ function determineLongestAnimationFromElementAnimations( } } -export const allLeavingAnimations = new Set(); +/** Tracks the IDs of LViews with leaving animations. */ +export const allLeavingAnimations = new Set(); diff --git a/packages/core/src/render3/instructions/animation.ts b/packages/core/src/render3/instructions/animation.ts index ea8f70bcb6c..a85677095ca 100644 --- a/packages/core/src/render3/instructions/animation.ts +++ b/packages/core/src/render3/instructions/animation.ts @@ -13,7 +13,7 @@ import { MAX_ANIMATION_TIMEOUT, } from '../../animation/interfaces'; import {getLView, getCurrentTNode} from '../state'; -import {RENDERER, INJECTOR, CONTEXT, LView} from '../interfaces/view'; +import {RENDERER, INJECTOR, CONTEXT, LView, ID} from '../interfaces/view'; import {getNativeByTNode} from '../util/view_utils'; import {performanceMarkFeature} from '../../util/performance'; import {Renderer} from '../interfaces/renderer'; @@ -277,7 +277,7 @@ function runLeaveAnimations( const renderer = lView[RENDERER]; const ngZone = lView[INJECTOR].get(NgZone); - allLeavingAnimations.add(lView); + allLeavingAnimations.add(lView[ID]); (getLViewLeaveAnimations(lView).get(tNode.index)!.resolvers ??= []).push(resolve); const activeClasses = getClassListFromValue(value); @@ -389,7 +389,7 @@ export function ɵɵanimateLeaveListener(value: AnimationFunction): typeof ɵɵa const tNode = getCurrentTNode()!; cancelLeavingNodes(tNode, lView); - allLeavingAnimations.add(lView); + allLeavingAnimations.add(lView[ID]); addAnimationToLView(getLViewLeaveAnimations(lView), tNode, () => runLeaveAnimationFunction(lView, tNode, value), diff --git a/packages/core/src/render3/instructions/control_flow.ts b/packages/core/src/render3/instructions/control_flow.ts index c38dec3ab8f..77339586aa1 100644 --- a/packages/core/src/render3/instructions/control_flow.ts +++ b/packages/core/src/render3/instructions/control_flow.ts @@ -28,6 +28,7 @@ import { DECLARATION_COMPONENT_VIEW, HEADER_OFFSET, HYDRATION, + ID, INJECTOR, LView, TVIEW, @@ -578,7 +579,7 @@ function clearDetachAnimationList(lContainer: LContainer, index: number): void { if (lContainer.length <= CONTAINER_HEADER_OFFSET) return; const indexInContainer = CONTAINER_HEADER_OFFSET + index; - const viewToDetach = lContainer[indexInContainer]; + const viewToDetach = lContainer[indexInContainer] as LView; const animations = viewToDetach ? (viewToDetach[ANIMATIONS] as AnimationLViewData | undefined) : undefined; @@ -590,7 +591,7 @@ function clearDetachAnimationList(lContainer: LContainer, index: number): void { ) { const injector = viewToDetach[INJECTOR]; removeFromAnimationQueue(injector, animations); - allLeavingAnimations.delete(viewToDetach); + allLeavingAnimations.delete(viewToDetach[ID]); animations.detachedLeaveAnimationFns = undefined; } } diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index 3212d7aabce..1e6f59afae1 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -76,6 +76,7 @@ import { TView, TViewType, INJECTOR, + ID, } from './interfaces/view'; import {assertTNodeType} from './node_assert'; import {profiler} from './profiler'; @@ -387,7 +388,7 @@ function runLeaveAnimationsWithCallback( if (animations == null || animations.leave == undefined || !animations.leave.has(tNode.index)) return callback(false); - if (lView) allLeavingAnimations.add(lView); + if (lView) allLeavingAnimations.add(lView[ID]); addToAnimationQueue( injector, @@ -410,7 +411,7 @@ function runLeaveAnimationsWithCallback( animations.running = Promise.allSettled(runningAnimations); runAfterLeaveAnimations(lView!, callback); } else { - if (lView) allLeavingAnimations.delete(lView); + if (lView) allLeavingAnimations.delete(lView[ID]); callback(false); } }, @@ -423,7 +424,7 @@ function runAfterLeaveAnimations(lView: LView, callback: Function) { if (runningAnimations) { runningAnimations.then(() => { lView[ANIMATIONS]!.running = undefined; - allLeavingAnimations.delete(lView); + allLeavingAnimations.delete(lView[ID]); callback(true); }); return;