refactor(core): Ensure determineLongestAnimation is run synchronously after style applies#67658
Conversation
atscott
left a comment
There was a problem hiding this comment.
PR 67658 Review Comments
AGENT: Thank you for this PR! The approach of deferring determineLongestAnimation to the next macro-task to avoid synchronous layout thrashing from getComputedStyle is a great performance optimization.
I have a couple of suggestions regarding the cleanup logic:
- Potential memory leak / late execution in
animateLeaveClassRunner:
InanimateLeaveClassRunner, the code pushes the fallback timeout cancellation tocleanupFnsinside the newsetTimeout:
fallbackTimeoutId = setTimeout(() => { ... }, longest.duration + 50) as unknown as number;
cleanupFns.push(() => clearTimeout(fallbackTimeoutId));If the component/view is destroyed before the setTimeout(..., 0) executes, the cleanupFns will be invoked by cleanupAfterLeaveAnimations(componentResolvers, cleanupFns) or during view destruction. At that time, the clearTimeout hasn't been added to cleanupFns yet.
When the setTimeout(..., 0) finally executes, it will create the fallbackTimeoutId, which will now never be cleaned up if the view is already gone. This means handleOutAnimationEnd will execute on a destroyed view duration + 50 ms later.
To fix this, we could track if the animation was cancelled (e.g. let isCleanedUp = false; cleanupFns.push(() => isCleanedUp = true; if (fallbackTimeoutId) clearTimeout(fallbackTimeoutId);)) and check if (hasCompleted || isCleanedUp) return; inside the setTimeout.
- Testing:
The Angular guidelines require that new logic or bug fixes include tests. I did a quick search through thepackagesdirectory and noticed that there don't appear to be any existing tests that explicitly cover thisdetermineLongestAnimationtimeout fallback logic (animation-fallbackevent). Is it possible to add a test case to cover this timing change? While testinggetComputedStylevsgetAnimations()directly might be tricky, we should ensure there are tests that verify that animations still apply smoothly and the cleanup works correctly with the new macro-task delay, to avoid any unintended regressions.
278cdc3 to
84491e6
Compare
… after style applies This adds a setTimeout, which guarantees that we call getAnimations one frame after a reflow is finished. This means getAnimations will return data, avoiding needing the expensive fallback of getComputedStyles. It also updates the cleanup to prevent a potential memory leak if the component is destroyed before the timeout runs.
84491e6 to
2c27793
Compare
atscott
left a comment
There was a problem hiding this comment.
AGENT: The changes look solid. The setTimeout inside the requestAnimationFrame avoids the expensive getComputedStyle fallback by ensuring getAnimations() correctly runs one frame after reflow. Both the completion and the early-unmount memory leak scenarios are appropriately guarded with isCleanedUp and verified by the tests. Approved.
| expect(cmp.el).toBeUndefined(); | ||
| })); | ||
|
|
||
| it('should fire the fallback timer and clean up if the animationend event is not dispatched', fakeAsync(() => { |
There was a problem hiding this comment.
Do we need to use fakeAsync for these? That seems unnecessary
There was a problem hiding this comment.
I get that there are long animations but they could be shorter
There was a problem hiding this comment.
I think it's just consistency. Every test in this file uses fakeAsync.
This adds a
setTimeout, which guarantees that we callgetAnimationsone frame after a reflow is finished. This meansgetAnimationswill return data, avoiding needing the expensive fallback of getComputedStyles.