-
Notifications
You must be signed in to change notification settings - Fork 25.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
perf(animations): improve algorithm to balance animation namespaces #45057
Conversation
d4fd692
to
f1c9c40
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As noted in #45055 (comment), the impact of this change is HUGE for applications that have large component trees with many components with animations.
@@ -151,12 +160,13 @@ if (_isNode || typeof Element !== 'undefined') { | |||
if (!isBrowser()) { | |||
_contains = (elm1, elm2) => elm1.contains(elm2); | |||
} else { | |||
_documentElement = /* @__PURE__ */ (() => document.documentElement)(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like this and welcome suggestions on how to do this differently.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just curious why do we need an iffe here (is it due to the top-level assignment not being optimized by Terser)? If so, may be we can add a quick comment here for future context?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a ban-toplevel-statement tslint rule (or a similar rule name, I don't recall by heart) that would fire if this is not wrapped inside an IIFE.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, got it. We do have some places where we disable that check via // tslint:disable-next-line: no-toplevel-property-access
. However in this particular case I'm not sure if Terser would be able to tree-shake-away that code without that iffe.
_documentElement = /* @__PURE__ */ (() => document.documentElement)(); | |
// tslint:disable-next-line: no-toplevel-property-access | |
_documentElement = /* @__PURE__ */ document.documentElement; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah exactly. I also checked whether we have existing suppressions but the ones I can across were for const enums. In this case a bundler would likely assume a potential side-effect for the property read and be able to optimize less aggressively.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for additional info. I think adding a quick comment (to explain why) here should be enough 👍
* be required to implement this method. This method is to become required in a major version of | ||
* Angular. | ||
*/ | ||
abstract getParentElement?(element: unknown): unknown; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder what the intended use of the AnimationDriver
public API really is (TBH I wonder why it's public API at all). If it's to allow custom implementations then this would have to be optional like I've done now, but if it's just for callers into this interface then we could just make this required from the start. I'm interested to hear other's thoughts on this.
I also went with unknown
even though it's a bit out of place compared to the other methods, but it seems silly to continue using any
when there's a safer alternative available.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After some digging in a prior PR, we discovered that AnimationDriver
is public API to allow people to write their own custom AnimationDriver implementations. I'm sure that's been done all of zero times, but the option is there. We could consider deprecating that.
I'm in favor of unknown
too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@JoostK Just wanted to check if you're planning an immediate follow-up that targets v14
to make this required. I think it's probably good to have this in patch as a perf fix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like to do that, yes. The impact of the breakage is likely negligible to warrant waiting for later majors, so I'll open a follow up to clean this up immediately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 🍪
reviewed-for: public-api, fw-testing, fw-core, fw-animations
* be required to implement this method. This method is to become required in a major version of | ||
* Angular. | ||
*/ | ||
abstract getParentElement?(element: unknown): unknown; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After some digging in a prior PR, we discovered that AnimationDriver
is public API to allow people to write their own custom AnimationDriver implementations. I'm sure that's been done all of zero times, but the option is there. We could consider deprecating that.
I'm in favor of unknown
too.
this._namespaceList.splice(i + 1, 0, ns); | ||
found = true; | ||
break; | ||
if (this.driver.getParentElement !== undefined) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a great optimization. I wish it didn't make this method so long.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reviewed-for: public-api
* be required to implement this method. This method is to become required in a major version of | ||
* Angular. | ||
*/ | ||
abstract getParentElement?(element: unknown): unknown; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@JoostK Just wanted to check if you're planning an immediate follow-up that targets v14
to make this required. I think it's probably good to have this in patch as a perf fix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@JoostK awesome investigation and the fix 👍
Reviewed-for: public-api
@@ -151,12 +160,13 @@ if (_isNode || typeof Element !== 'undefined') { | |||
if (!isBrowser()) { | |||
_contains = (elm1, elm2) => elm1.contains(elm2); | |||
} else { | |||
_documentElement = /* @__PURE__ */ (() => document.documentElement)(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just curious why do we need an iffe here (is it due to the top-level assignment not being optimized by Terser)? If so, may be we can add a quick comment here for future context?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reviewed-for: public-api
f1c9c40
to
49ecee7
Compare
49ecee7
to
c09ca47
Compare
c09ca47
to
d5ee1ba
Compare
Another rebase tipped this beyond a bundle size limit, so this needs another round of approvals. For context, I'll open a PR targeting |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reviewed-for: size-tracking
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed-for: size-tracking
@@ -592,16 +592,40 @@ export class TransitionAnimationEngine { | |||
const limit = this._namespaceList.length - 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Quick comment on the ~700 bytes increase in goldens/size-tracking/aio-payloads.json
: this improvement is definitely worth it 👍
One small improvement/refactoring that we can make in this function (to offset some of this increase) is to extract commonly accessed fields into a local var (since property names are not mangled by our build pipeline):
const nsList = this._namespaceList;
const nsByHostElement = this.namespacesByHostElement;
const getParentElement = this.driver.getParentElement;
It'd be minified to something like this:
var a = t._namespaceList,
b = t.namespacesByHostElement,
c = t.driver.getParentElement;
So the long names will not be present in the function body.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've extracted namespaceList
and namespacesByHostElement
into local variables, that is indeed a clever idea. I've kept the getParentElement
reference as is, as changing that into a local variable requires calling it using .call(this.driver, ...)
which isn't ideal.
The prior approach would consider all existing namespaces from back to front to find the one that's the closest ancestor for a given host element. An expensive `contains` operation was used which needed to traverse all the way up the document root _for each existing namespace_. This commit implements an optimization where the closest namespace is found by traversing up from the host element, avoiding repeated DOM traversal. Closes angular#45055
d5ee1ba
to
ff85a96
Compare
FYI, this doesn't merge cleanly to |
This PR was merged into the repository by commit 5c7c56b. |
This change is a follow up to angular#45057 to make `AnimationDriver.getParentElement` a required method, which allows removing the slow path for the animation namespace insertion logic. BREAKING CHANGE: The `AnimationDriver.getParentElement` method has become required, so any implementors of this interface are now required to provide an implementation for this method. This breakage is unlikely to affect application developers, as `AnimationDriver` is not expected to be implemented in user code.
This change is a follow up to angular#45057 to make `AnimationDriver.getParentElement` a required method, which allows removing the slow path for the animation namespace insertion logic. BREAKING CHANGE: The `AnimationDriver.getParentElement` method has become required, so any implementors of this interface are now required to provide an implementation for this method. This breakage is unlikely to affect application developers, as `AnimationDriver` is not expected to be implemented in user code.
This change is a follow up to angular#45057 to make `AnimationDriver.getParentElement` a required method, which allows removing the slow path for the animation namespace insertion logic. BREAKING CHANGE: The `AnimationDriver.getParentElement` method has become required, so any implementors of this interface are now required to provide an implementation for this method. This breakage is unlikely to affect application developers, as `AnimationDriver` is not expected to be implemented in user code.
#45114) This change is a follow up to #45057 to make `AnimationDriver.getParentElement` a required method, which allows removing the slow path for the animation namespace insertion logic. BREAKING CHANGE: The `AnimationDriver.getParentElement` method has become required, so any implementors of this interface are now required to provide an implementation for this method. This breakage is unlikely to affect application developers, as `AnimationDriver` is not expected to be implemented in user code. PR Close #45114
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
…ngular#45057) The prior approach would consider all existing namespaces from back to front to find the one that's the closest ancestor for a given host element. An expensive `contains` operation was used which needed to traverse all the way up the document root _for each existing namespace_. This commit implements an optimization where the closest namespace is found by traversing up from the host element, avoiding repeated DOM traversal. Closes angular#45055 PR Close angular#45057
The prior approach would consider all existing namespaces from back to front
to find the one that's the closest ancestor for a given host element. An
expensive
contains
operation was used which needed to traverse all theway up the document root for each existing namespace. This commit implements
an optimization where the closest namespace is found by traversing up from
the host element, avoiding repeated DOM traversal.
Closes #45055