Skip to content

perf(platform-browser): disable styles of removed components instead of removing#64524

Open
mattlewis92 wants to merge 7 commits intoangular:mainfrom
mattlewis92:issue-59651
Open

perf(platform-browser): disable styles of removed components instead of removing#64524
mattlewis92 wants to merge 7 commits intoangular:mainfrom
mattlewis92:issue-59651

Conversation

@mattlewis92
Copy link
Copy Markdown
Contributor

@mattlewis92 mattlewis92 commented Oct 19, 2025

This commit changes the behaviour of REMOVE_STYLES_ON_COMPONENT_DESTROY.

Now, style and link nodes are disabled instead of removed from DOM which avoiding the browser having to reparse the stylesheet when the stylesheet is re-enabled.

This was previously reverted in 1640743 but it seems like the observed perf regression in chromium has since been fixed in https://issues.chromium.org/issues/40911913, so it should be possible to reland this perf improvement

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • angular.dev application / infrastructure changes
  • Other... Please describe:

What is the current behavior?

When all components using a stylesheet are destroyed, the stylesheet is removed from the DOM

Issue Number: 59651

What is the new behavior?

The stylesheet is now marked as disabled, which improves performance the next time the stylesheet is enabled, as the browser will not have to incur the cost of reparsing the stylesheet when enabling it (although it will still trigger style recalculations)

Does this PR introduce a breaking change?

  • Yes
  • No

When a component is destroyed, its associated <style> elements are now
disabled in the DOM rather than removed. When the same component is recreated, the styles
are re-enabled at their original position instead of being re-appended to the end of
<head>. This may change the CSS cascade order of component stylesheets, which could
affect styling if your application relies on the order in which component styles appear
in the DOM.

To migrate, avoid depending on the insertion order of Angular component stylesheets in
<head>. If you have styles that must take precedence over component styles, use more
specific CSS selectors or increase specificity rather than relying on source order.
Applications that do not rely on stylesheet ordering in <head> are not affected.

Other information

@pullapprove pullapprove bot requested a review from crisbeto October 19, 2025 16:47
@angular-robot angular-robot bot added area: performance Issues related to performance area: core Issues related to the framework runtime labels Oct 19, 2025
@ngbot ngbot bot added this to the Backlog milestone Oct 19, 2025
@JeanMeche JeanMeche requested a review from alan-agius4 October 19, 2025 17:25
@alan-agius4 alan-agius4 added the requires: TGP This PR requires a passing TGP before merging is allowed label Oct 19, 2025
@pullapprove pullapprove bot removed the requires: TGP This PR requires a passing TGP before merging is allowed label Oct 19, 2025
*/
interface UsageRecord<T> {
elements: T[];
elements: Map</** Host */ Node, /** Style Node */ T>;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why do we need a Map here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It was the data structure that was ported from the original commit that was reverted:

elements: Map</** Host */ Node, /** Style Node */ HTMLStyleElement>;

What would you propose instead, just a plain object?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This code has been refactored greatly over the past year or so, and from what I am seeing elements: T[]; can be retained as the data structure as mostly I am seeing elements.values() which makes using a Map redundant here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

What about here when we remove the host?

for (const {elements} of [...this.inline.values(), ...this.external.values()]) {
elements.delete(hostNode);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

the removeHost is used used only in the ShadowDomRenderer, and in this case both this.inline and this.external will not be populated for the host that was added via addHostas addStyles and removeStyles are not used for shadow dom.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Makes sense, thank you! Fixed in 035ae4a (#64524)

@crisbeto crisbeto removed their request for review October 20, 2025 07:17
@pullapprove pullapprove bot requested a review from crisbeto October 20, 2025 07:17
@mattlewis92 mattlewis92 force-pushed the issue-59651 branch 2 times, most recently from 9ee205d to 035ae4a Compare October 20, 2025 15:15
@thePunderWoman
Copy link
Copy Markdown
Contributor

@mattlewis92 This will likely have an impact on the new animate.enter and animate.leave APIs. We have issues where style sheets were getting prematurely pruned before when elements were removed. Unfortunately this is not the type of behavior that's easy to unit test. Can you verify this has no impact on that use case? We don't want styles disabled for elements that are in the process of animating away.

@mattlewis92
Copy link
Copy Markdown
Contributor Author

@mattlewis92 This will likely have an impact on the new animate.enter and animate.leave APIs. We have issues where style sheets were getting prematurely pruned before when elements were removed. Unfortunately this is not the type of behavior that's easy to unit test. Can you verify this has no impact on that use case? We don't want styles disabled for elements that are in the process of animating away.

Sure I can test that - do you have any kind of animations playground setup / reproduction of the previous issue where I can test it against?

Copy link
Copy Markdown
Contributor

@thePunderWoman thePunderWoman left a comment

Choose a reason for hiding this comment

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

This looks good to me. I think since you're re-using existing codepaths, this shouldn't have any impact on the animations use case. So we should be good.

@JeanMeche JeanMeche added the action: global presubmit The PR is in need of a google3 global presubmit label Oct 24, 2025
@JeanMeche
Copy link
Copy Markdown
Member

I'll take care of running a TGP over the weekend.

@JeanMeche JeanMeche added state: blocked on G3 cleanup This change requires a G3 cleanup state: blocked and removed state: blocked on G3 cleanup This change requires a G3 cleanup labels Oct 27, 2025
@JeanMeche
Copy link
Copy Markdown
Member

TGP is failing. removeStyles has some usages, because even though ɵSharedStylesHost is private its pulled by at least one project.

@mattlewis92
Copy link
Copy Markdown
Contributor Author

TGP is failing. removeStyles has some usages, because even though ɵSharedStylesHost is private its pulled by at least one project.

Would it make sense to rename disableStyles back to removeStyles, or are you able to easily clean this up internally?

@thePunderWoman
Copy link
Copy Markdown
Contributor

Heads up that this is likely to end up conflicting with #66048 which is being implemented by @dgp1130.

@mattlewis92
Copy link
Copy Markdown
Contributor Author

@thePunderWoman was the internal usage of the addStyles api cleaned up yet to unblock this PR? Looks like the signature of addStyles is changing in #66048 as well, so I guess it will need to be cleaned up to unblock both PRs

@thePunderWoman
Copy link
Copy Markdown
Contributor

@mattlewis92 I'm unaware of the addStyles cleanup issue. Is there an open issue associated?

@mattlewis92
Copy link
Copy Markdown
Contributor Author

@mattlewis92 I'm unaware of the addStyles cleanup issue. Is there an open issue associated?

Just basing it on this comment from above:

TGP is failing. removeStyles has some usages, because even though ɵSharedStylesHost is private its pulled by at least one project.

@thePunderWoman
Copy link
Copy Markdown
Contributor

@mattlewis92 Ah, I see. No, to my knowledge that has not changed.

@mattlewis92
Copy link
Copy Markdown
Contributor Author

@JeanMeche ah I see the issue, on HMR it's leaving the old style element disabled and creating a new one

CleanShot 2026-03-06 at 12 18 39@2x

@JeanMeche
Copy link
Copy Markdown
Member

It was raised by an internal test, I haven't dug too much into it but I looks like to me it was reproducing what happened at runtime.

That might be what you were able to reproduce.

@mattlewis92
Copy link
Copy Markdown
Contributor Author

@JeanMeche OK this looks like it fixes it (verified in a real app): 9156d36

Please can you kick off the G3 tests again and let me know if the test passes now?

@JeanMeche
Copy link
Copy Markdown
Member

Thx, I'll keep you updated.

@mattlewis92
Copy link
Copy Markdown
Contributor Author

All failing tests in CI pass locally, so I guess they're just flakes?

@JeanMeche
Copy link
Copy Markdown
Member

You might have only run the node test targets and the not browser test targets ?
pnpm bazel test //packages/core/test/acceptance:acceptance_web_chromium should pass locally (the firefox are always broken locally), It fails for me.

@mattlewis92
Copy link
Copy Markdown
Contributor Author

OK I think it's fixed now - the remaining test passes on my local machine, so I guess it's flakey?

Starting local Bazel server (8.6.0) and connecting to it...
INFO: Analyzed target //devtools/projects/ng-devtools-backend/src/lib/component-tree:test_firefox (1054 packages loaded, 47300 targets configured).
INFO: Found 1 test target...
Target //devtools/projects/ng-devtools-backend/src/lib/component-tree:test_firefox up-to-date:
  dist/bin/devtools/projects/ng-devtools-backend/src/lib/component-tree/test_firefox_/test_firefox
INFO: Elapsed time: 147.040s, Critical Path: 40.79s
INFO: 7703 processes: 2400 internal, 1466 darwin-sandbox, 3548 local, 289 worker.
INFO: Build completed successfully, 7703 total actions
//devtools/projects/ng-devtools-backend/src/lib/component-tree:test_firefox PASSED in 8.8s

Executed 1 out of 1 test: 1 test passes.

@JeanMeche
Copy link
Copy Markdown
Member

This one is flakey yes.

@mattlewis92
Copy link
Copy Markdown
Contributor Author

Looks like the presubmit passed! 🚀

@JeanMeche
Copy link
Copy Markdown
Member

I'm seeing some slight changes on some screenshot tests but I wouldn't consider them minor. No sure they're flakey.

@mattlewis92
Copy link
Copy Markdown
Contributor Author

Anything you're able to share to help me debug it? (I'm in the angular slack if you want to DM it to keep it private)

@JeanMeche
Copy link
Copy Markdown
Member

Had a quick chat with Matt, this is probably a breaking change as the order of the styles elements might be different with this change.

Can you update the commit and the PR to reflect that Matt ? Thanks

@angular-robot angular-robot bot added the detected: breaking change PR contains a commit with a breaking change label Mar 9, 2026
@mattlewis92
Copy link
Copy Markdown
Contributor Author

Done!

@mattlewis92
Copy link
Copy Markdown
Contributor Author

The CI failure looks unrelated to this change, it wasn't failing on Friday, and I don't think this PR added more than 5kb of code to exceed the bundle budget threshold

…of removing

This commit changes the behaviour of `REMOVE_STYLES_ON_COMPONENT_DESTROY`.

Now, `style` and `link` nodes are disabled instead of removed from DOM which avoiding the browser having to reparse the stylesheet when the stylesheet is re-enabled.

This was previously reverted in 1640743 but it seems like the observed perf regression in chromium has since been fixed in https://issues.chromium.org/issues/40911913, so it should be possible to reland this perf improvement

BREAKING CHANGE: When a component is destroyed, its associated `<style>` elements are now
disabled in the DOM rather than removed. When the same component is recreated, the styles
are re-enabled at their original position instead of being re-appended to the end of
`<head>`. This may change the CSS cascade order of component stylesheets, which could
affect styling if your application relies on the order in which component styles appear
in the DOM.

To migrate, avoid depending on the insertion order of Angular component stylesheets in
`<head>`. If you have styles that must take precedence over component styles, use more
specific CSS selectors or increase specificity rather than relying on source order.
Applications that do not rely on stylesheet ordering in `<head>` are not affected.
…nstead of removing

Clean up stale disabled stylesheets during HMR when component styles change.
…nstead of removing

Prevent deferred animation renderer destroy from disabling replacement styles during HMR.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

action: global presubmit The PR is in need of a google3 global presubmit area: core Issues related to the framework runtime area: performance Issues related to performance breaking changes detected: breaking change PR contains a commit with a breaking change state: blocked

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Re-land disabling component styles instead of removing them

4 participants