Repository navigation
Description
Which @angular/* package(s) are the source of the bug?
core
Is this a regression?
No
Description
Destroying a resource() calls AbortController.abort() without a reason. Per the DOM Standard that creates a new "AbortError" DOMException for every destroyed resource, and in Chromium creating it captures a JavaScript stack trace whose cost grows with the depth of the call stack. Resources are destroyed inside the view destroy recursion, where the stack is deep, so this adds up when a page with many resources is torn down.
Where it happens (main at e0260db):
ResourceImplcreates anAbortControllerper load and keeps it after the load has resolved: resource.ts#L442destroy()callsabortInProgressLoad(): resource.ts#L397-L401abortInProgressLoad()aborts without a reason: resource.ts#L519-L520rxResourcerelies on this abort to unsubscribe its Observable: rx_resource.ts#L89-L100
So every resource that has loaded once calls abort() when it is destroyed.
What the spec says:
abort(reason): "The abort(reason) method steps are to signal abort on this with reason if it is given."- signal abort, step 2: "Set signal's abort reason to reason if it is given; otherwise to a new "AbortError" DOMException."
Measurements (Chromium 153 via Playwright 1.63, 4× CPU throttling, default Error.stackTraceLimit of 10), cost per call:
| Call stack depth | abort() |
abort(reason) |
|---|---|---|
| 5 | 14 µs | 0.8 µs |
| 100 | 50 µs | 1.8 µs |
| 300 | 150 µs | 1.0 µs |
In our application, navigating away from a page destroys 88–95 resources. Their abort() calls take 24–35 ms per navigation (4× throttling, about 300 µs per call inside the destroy recursion). With a shared reason (see the workaround below) they take 5–10 ms; the rest is the abort listeners themselves (unsubscribing).
Possible fix: pass a reason, for example one DOMException('...', 'AbortError') created once at module level. ResourceImpl itself only checks abortSignal.aborted, and loaders that forward the signal (e.g. to fetch) still see an AbortError as the reason, so the observable behaviour stays the same apart from the shared instance.
Workaround we use until then, in the application bootstrap:
const sharedAbortReason = new DOMException('signal is aborted without reason', 'AbortError');
const nativeAbort = AbortController.prototype.abort;
AbortController.prototype.abort = function (reason?: unknown) {
nativeAbort.call(this, reason === undefined ? sharedAbortReason : reason);
};Please provide a link to a minimal reproduction of the bug
No StackBlitz. The cost can be reproduced in any Chromium console, independent of Angular:
const n = 500;
const atDepth = (depth, fn) => (depth === 0 ? fn() : atDepth(depth - 1, fn));
const measure = (depth, op) =>
atDepth(depth, () => {
const controllers = Array.from({ length: n }, () => new AbortController());
const start = performance.now();
for (const c of controllers) op(c);
return `${(((performance.now() - start) / n) * 1000).toFixed(1)} µs`;
});
for (const depth of [5, 100, 300]) {
console.log(depth, 'abort():', measure(depth, (c) => c.abort()), 'abort(reason):', measure(depth, (c) => c.abort('destroyed')));
}On the Angular side, it shows up whenever a view tree with many resource()/rxResource() instances is destroyed, e.g. on a route change.
Please provide the exception or error you saw
No error. Per destroyed resource, an AbortError DOMException including a stack trace is created.
Please provide the environment you discovered this bug in (run ng version)
Angular: 22.1.8 (the code above is unchanged on main e0260db)
Node: 24.18.0
Browser: Chromium 153 (Playwright 1.63)
Anything else?
No response
Activity
demike commentedon Oct 19, 2026
Some more numbers, to show how this scales. Measured in a blank page (Chromium 153), with and without CPU throttling; the table in the description is the 4× column (throttled to emulate a low-end device, which is where we noticed it).
Cost per call by call stack depth:
abort()1×abort(reason)1×abort()4×abort(reason)4×Total time by number of destroyed resources (one abort listener each, like
rxResource; depth 300; mean of 20 runs):abort()1×abort(reason)1×abort()4×abort(reason)4×So the cost grows linearly with the number of resources, about 30 µs per resource on a desktop CPU and about 160 µs on a low-end device, while
abort(reason)stays negligible. In our application ~90 resources are destroyed per navigation (24–35 ms with 4× throttling).Snippet for the console (use the Performance panel's CPU throttling for the 4× numbers):
perf(core): avoid unnecessary aborts and reuse abort reason during re…
perf(core): avoid unnecessary aborts and reuse abort reason during re…
perf(core): avoid unnecessary aborts and reuse abort reason during re…