Visitar URL original
perf(router): do not retain UrlTree instances in RouterLink · angular/angular@14793bf · GitHub
Skip to content

Commit 14793bf

Browse files
committed
perf(router): do not retain UrlTree instances in RouterLink
In 89d47d8, RouterLink internals were refactored to use signals, introducing a computed signal `_urlTree`. Because every `<a routerLink>` maintains an active RouterLink directive instance for the lifetime of the view/request, the computed signal permanently cached the returned UrlTree (and its cloned queryParams map and segment tree) in `ReactiveNode.value` for every link. During server-side rendering (SSR), especially when query parameters are large or when `defaultQueryParamsHandling: 'merge'` is enabled, each rendered link held a full copy of the query parameter map pinned in the heap. In an application with 100 links and thousands of query parameters, this pinned over 50 MB of heap per in-flight request, causing server workers to crash with Out of Memory (OOM) under concurrent load (#70908). This change moves the reactive dependencies (`path`, `fragment`, and `queryParams`) directly into `reactiveHref` so it computes and retains only the serialized href string. The intermediate UrlTree and its queryParams dictionary are discarded immediately after serialization and collected during young-generation GC scavenges rather than remaining pinned in the heap. The public `urlTree` getter and `onClick` handler now compute the tree on demand, restoring the memory profile prior to 89d47d8. Fixes #70908 (cherry picked from commit 319a3c4)
1 parent b9a8605 commit 14793bf

1 file changed

Lines changed: 40 additions & 47 deletions

File tree

‎packages/router/src/directives/router_link.ts‎

Lines changed: 40 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,20 @@ export class RouterLink implements OnChanges, OnDestroy {
195195
if (!this.isAnchorElement) {
196196
return this.hrefAttributeValue;
197197
}
198-
return this.computeHref(this._urlTree());
198+
// Track path changes. It's knowing which segments we actually depend on is somewhat difficult
199+
this.reactiveRouterState.path();
200+
if (this._preserveFragment()) {
201+
this.reactiveRouterState.fragment();
202+
}
203+
const shouldTrackParams = (handling: QueryParamsHandling | undefined | null) =>
204+
handling === 'preserve' || handling === 'merge';
205+
if (
206+
shouldTrackParams(this._queryParamsHandling()) ||
207+
shouldTrackParams(this.options?.defaultQueryParamsHandling)
208+
) {
209+
this.reactiveRouterState.queryParams();
210+
}
211+
return this.computeHref(this.createUrlTree());
199212
});
200213
/**
201214
* Represents an `href` attribute value applied to a host element,
@@ -388,15 +401,13 @@ export class RouterLink implements OnChanges, OnDestroy {
388401
!!(
389402
// Avoid breaking in an SSR context where customElements might not
390403
// be defined.
404+
typeof customElements === 'object' &&
405+
// observedAttributes is an optional static property/getter on a
406+
// custom element. The spec states that this must be an array of
407+
// strings.
391408
(
392-
typeof customElements === 'object' &&
393-
// observedAttributes is an optional static property/getter on a
394-
// custom element. The spec states that this must be an array of
395-
// strings.
396-
(
397-
customElements.get(tagName) as {observedAttributes?: string[]} | undefined
398-
)?.observedAttributes?.includes?.('href')
399-
)
409+
customElements.get(tagName) as {observedAttributes?: string[]} | undefined
410+
)?.observedAttributes?.includes?.('href')
400411
);
401412

402413
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
@@ -483,7 +494,7 @@ export class RouterLink implements OnChanges, OnDestroy {
483494
altKey: boolean,
484495
metaKey: boolean,
485496
): boolean {
486-
const urlTree = this._urlTree();
497+
const urlTree = this.urlTree;
487498

488499
if (urlTree === null) {
489500
return true;
@@ -534,45 +545,27 @@ export class RouterLink implements OnChanges, OnDestroy {
534545
}
535546
}
536547

537-
/** @internal */
538-
_urlTree = computed(
539-
() => {
540-
// Track path changes. It's knowing which segments we actually depend on is somewhat difficult
541-
this.reactiveRouterState.path();
542-
if (this._preserveFragment()) {
543-
this.reactiveRouterState.fragment();
544-
}
545-
const shouldTrackParams = (handling: QueryParamsHandling | undefined | null) =>
546-
handling === 'preserve' || handling === 'merge';
547-
if (
548-
shouldTrackParams(this._queryParamsHandling()) ||
549-
shouldTrackParams(this.options?.defaultQueryParamsHandling)
550-
) {
551-
this.reactiveRouterState.queryParams();
552-
}
553-
554-
const routerLinkInput = this.routerLinkInput();
555-
if (routerLinkInput === null || !this.router.createUrlTree) {
556-
return null;
557-
} else if (isUrlTree(routerLinkInput)) {
558-
return routerLinkInput;
559-
}
560-
return this.router.createUrlTree(routerLinkInput, {
561-
// If the `relativeTo` input is not defined, we want to use `this.route`
562-
// by default.
563-
// Otherwise, we should use the value provided by the user in the input.
564-
relativeTo: this._relativeTo() !== undefined ? this._relativeTo() : this.route,
565-
queryParams: this._queryParams(),
566-
fragment: this._fragment(),
567-
queryParamsHandling: this._queryParamsHandling(),
568-
preserveFragment: this._preserveFragment(),
569-
});
570-
},
571-
{equal: (a, b) => this.computeHref(a) === this.computeHref(b)},
572-
);
548+
private createUrlTree(): UrlTree | null {
549+
const routerLinkInput = this.routerLinkInput();
550+
if (routerLinkInput === null || !this.router.createUrlTree) {
551+
return null;
552+
} else if (isUrlTree(routerLinkInput)) {
553+
return routerLinkInput;
554+
}
555+
return this.router.createUrlTree(routerLinkInput, {
556+
// If the `relativeTo` input is not defined, we want to use `this.route`
557+
// by default.
558+
// Otherwise, we should use the value provided by the user in the input.
559+
relativeTo: this._relativeTo() !== undefined ? this._relativeTo() : this.route,
560+
queryParams: this._queryParams(),
561+
fragment: this._fragment(),
562+
queryParamsHandling: this._queryParamsHandling(),
563+
preserveFragment: this._preserveFragment(),
564+
});
565+
}
573566

574567
get urlTree(): UrlTree | null {
575-
return untracked(this._urlTree);
568+
return untracked(() => this.createUrlTree());
576569
}
577570

578571
private computeHref(urlTree: UrlTree | null): string | null {

0 commit comments

Comments
 (0)