Skip to content

perf(core): avoid unnecessary aborts and reuse abort reason during resource teardown - #71557

Merged
thePunderWoman merged 1 commit into
angular:mainfrom
JeanMeche:perf/resource-abort
Nov 10, 2026
Merged

thePunderWoman merged 1 commit into
angular:mainfrom
JeanMeche:perf/resource-abort

Conversation

@JeanMeche

Copy link
Copy Markdown
Member

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: Performance optimization

What is the current behavior?

When tearing down a view tree containing resources (e.g. during route navigation), ResourceImpl.destroy() invokes abortInProgressLoad(), calling pendingController.abort() without arguments.

This leads to significant teardown latency due to two factors:

  1. Per the WHATWG DOM spec, calling abort() with no reason instantiates a new "AbortError" DOMException. In V8/Chromium, DOMException construction triggers v8::Exception::CaptureStackTrace(). The cost of walking execution stack frames scales linearly with call stack depth (~140 µs per call at depth 300 on throttled CPUs). When destroying dozens of resources in deep view trees, this adds 20–40ms of main thread blockage.
  2. ResourceImpl retains its pendingController even after a one-shot (non-streaming) loader has settled (resolved or errored). When the component is eventually destroyed, abort() is executed on controllers whose operations finished long ago.

Issue Number: #71422

What is the new behavior?

  1. Reused Abort Reason:
    abortInProgressLoad() now passes a shared, lazily-initialized DOMException('The operation was aborted.', 'AbortError'). Passing an existing DOMException prevents V8 from capturing a fresh call stack on every call while preserving complete Web API and consumer compatibility (err.name === 'AbortError', err instanceof DOMException).
  2. Clear Pending Controller on Completion:
    For non-streaming resources (PromiseResourceOptions), once the loader promise settles (in finally), pendingController is cleared if it belongs to that settled load. When the resource is destroyed later, teardown does not perform unnecessary abort work or event dispatch. Streaming resources (like rxResource or StreamingResourceOptions) retain their controller so active subscriptions and streams continue to be cleanly aborted on destroy.

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

Draft PR proposing the two-pronged optimization discussed in #71422 and #71423.

@angular-robot angular-robot Bot added area: performance Issues related to performance area: core Issues related to the framework runtime labels Oct 23, 2026
@ngbot ngbot Bot added this to the Backlog milestone Oct 23, 2026
@JeanMeche
JeanMeche marked this pull request as ready for review October 23, 2026 01:39
@JeanMeche
JeanMeche requested a review from leonsenft October 23, 2026 01:40
Comment thread packages/core/src/resource/resource.ts Outdated
options.injector ?? inject(Injector),
options.id as StateKey<T>,
undefined,
isStreamingResourceOptions(options),

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

HttpResourceImpl extends ResourceImpl this is already being taken care of.

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.

I meant should we not pass in isStreaming (since it defaults to false) to super() in the HttpResourceImpl constructor?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The httpResource isn't a streaming resource per se, so false is the right value.

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.

I see. I was confused by the naming because it implements and passes a getInitialStream calback, but does that refer to a different kind of stream?

Comment thread packages/core/src/resource/resource.ts Resolved
…source teardown

When tearing down component trees containing resources, `abortInProgressLoad` previously invoked `pendingController.abort()` without arguments for every resource.

This caused two performance issues:
1. Omitting the reason parameter in `abort()` instantiates a new `DOMException` ('AbortError') each time. In engines like V8, this triggers a full call stack capture (`CaptureStackTrace()`), whose cost scales linearly with call stack depth. On deep component tree teardown, this can take up to 20-40ms.
2. `ResourceImpl` retained its `pendingController` even after a non-streaming load had completed, unnecessarily executing `abort()` for operations that were no longer in flight.

This change:
- Reuses a shared `AbortError` `DOMException` reason when aborting loads, preventing repeated V8 stack captures while maintaining full compatibility with consumers expecting a standard `DOMException`.
- Clears `pendingController` upon completion for non-streaming resources, avoiding unnecessary abort work when tearing down already settled resources. Streaming resources (such as `rxResource`) continue to receive abort signals on destroy to clean up active subscriptions.

Fixes angular#71422
@JeanMeche
JeanMeche force-pushed the perf/resource-abort branch from 8cee1fc to 09fe7ec Compare October 28, 2026 02:15
@JeanMeche
JeanMeche requested a review from leonsenft October 28, 2026 02:20
@JeanMeche JeanMeche added action: merge The PR is ready for merge by the caretaker target: patch This PR is targeted for the next patch release labels Oct 29, 2026
@thePunderWoman
thePunderWoman merged commit 833c1f2 into angular:main Nov 10, 2026
25 of 28 checks passed
@thePunderWoman

Copy link
Copy Markdown
Contributor

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 area: performance Issues related to performance 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