Skip to content

refactor(core): Ensure determineLongestAnimation is run synchronously after style applies#67658

Merged
mattrbeck merged 1 commit intoangular:mainfrom
thePunderWoman:animation-reflow
Mar 13, 2026
Merged

refactor(core): Ensure determineLongestAnimation is run synchronously after style applies#67658
mattrbeck merged 1 commit intoangular:mainfrom
thePunderWoman:animation-reflow

Conversation

@thePunderWoman
Copy link
Contributor

@thePunderWoman thePunderWoman commented Mar 12, 2026

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.

@thePunderWoman thePunderWoman added action: review The PR is still awaiting reviews from at least one requested reviewer area: core Issues related to the framework runtime target: patch This PR is targeted for the next patch release labels Mar 12, 2026
@ngbot ngbot bot added this to the Backlog milestone Mar 12, 2026
@thePunderWoman thePunderWoman requested a review from atscott March 12, 2026 20:46
Copy link
Contributor

@atscott atscott left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Potential memory leak / late execution in animateLeaveClassRunner:
    In animateLeaveClassRunner, the code pushes the fallback timeout cancellation to cleanupFns inside the new setTimeout:
        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.

  1. Testing:
    The Angular guidelines require that new logic or bug fixes include tests. I did a quick search through the packages directory and noticed that there don't appear to be any existing tests that explicitly cover this determineLongestAnimation timeout fallback logic (animation-fallback event). Is it possible to add a test case to cover this timing change? While testing getComputedStyle vs getAnimations() 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.

… 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.
Copy link
Contributor

@atscott atscott left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@thePunderWoman thePunderWoman added action: merge The PR is ready for merge by the caretaker and removed action: review The PR is still awaiting reviews from at least one requested reviewer labels Mar 12, 2026
expect(cmp.el).toBeUndefined();
}));

it('should fire the fallback timer and clean up if the animationend event is not dispatched', fakeAsync(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to use fakeAsync for these? That seems unnecessary

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get that there are long animations but they could be shorter

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's just consistency. Every test in this file uses fakeAsync.

@mattrbeck mattrbeck merged commit 318ade0 into angular:main Mar 13, 2026
21 checks passed
@mattrbeck
Copy link
Member

This PR was merged into the repository. The changes were merged into the following branches:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

action: merge The PR is ready for merge by the caretaker area: core Issues related to the framework runtime target: patch This PR is targeted for the next patch release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants