From a4d16130b87796234cb9a9d44ab3c40342a5d708 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 23 Sep 2025 10:26:33 +0000 Subject: [PATCH] fix(@angular/ssr): avoid retaining rendered HTML in memory post-request The previous implementation for server-side rendering could lead to memory leaks where the rendered HTML content was not properly garbage-collected after the HTTP request was fulfilled. This was caused by inefficiencies in the critical CSS caching and how the response stream was handled. This commit addresses the issue by: 1. Refactoring the `criticalCssLRUCache` to store `Uint8Array` directly and use a more robust caching strategy. 2. Using the request URL as the cache key to prevent storing multiple cache entries for the same resource. 3. Ensuring the response stream controller enqueues the final HTML as a `Uint8Array`, avoiding unnecessary string conversions and dangling references. These changes prevent the SSR response from being retained in memory, improving the stability and performance of the server. Closes #31277 --- packages/angular/ssr/src/app.ts | 77 +++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 28 deletions(-) diff --git a/packages/angular/ssr/src/app.ts b/packages/angular/ssr/src/app.ts index dd90999d287a..4895866d715b 100644 --- a/packages/angular/ssr/src/app.ts +++ b/packages/angular/ssr/src/app.ts @@ -143,13 +143,15 @@ export class AngularServerApp { private readonly textDecoder = new TextEncoder(); /** - * Cache for storing critical CSS for pages. - * Stores a maximum of MAX_INLINE_CSS_CACHE_ENTRIES entries. + * A cache that stores critical CSS to avoid re-processing for every request, improving performance. + * This cache uses a Least Recently Used (LRU) eviction policy. * - * Uses an LRU (Least Recently Used) eviction policy, meaning that when the cache is full, - * the least recently accessed page's critical CSS will be removed to make space for new entries. + * @see {@link MAX_INLINE_CSS_CACHE_ENTRIES} for the maximum number of entries this cache can hold. */ - private readonly criticalCssLRUCache = new LRUCache(MAX_INLINE_CSS_CACHE_ENTRIES); + private readonly criticalCssLRUCache = new LRUCache< + string, + { shaOfContentPreInlinedCss: string; contentWithCriticialCSS: Uint8Array } + >(MAX_INLINE_CSS_CACHE_ENTRIES); /** * Handles an incoming HTTP request by serving prerendered content, performing server-side rendering, @@ -198,7 +200,6 @@ export class AngularServerApp { * * @param request - The incoming HTTP request for serving a static page. * @param matchedRoute - The metadata of the matched route for rendering. - * If not provided, the method attempts to find a matching route based on the request URL. * @returns A promise that resolves to a `Response` object if the prerendered page is found, or `null`. */ private async handleServe( @@ -247,7 +248,6 @@ export class AngularServerApp { * * @param request - The incoming HTTP request to be processed. * @param matchedRoute - The metadata of the matched route for rendering. - * If not provided, the method attempts to find a matching route based on the request URL. * @param requestContext - Optional additional context for rendering, such as request metadata. * * @returns A promise that resolves to the rendered response, or null if no matching route is found. @@ -343,8 +343,8 @@ export class AngularServerApp { const stream = new ReadableStream({ start: async (controller) => { const renderedHtml = await result.content(); - const finalHtml = await this.inlineCriticalCss(renderedHtml, url, true); - controller.enqueue(this.textDecoder.encode(finalHtml)); + const finalHtml = await this.inlineCriticalCssWithCache(renderedHtml, url); + controller.enqueue(finalHtml); controller.close(); }, }); @@ -355,33 +355,19 @@ export class AngularServerApp { /** * Inlines critical CSS into the given HTML content. * - * @param html - The HTML content to process. - * @param url - The URL associated with the request, for logging purposes. - * @param cache - A flag to indicate if the result should be cached. + * @param html The HTML content to process. + * @param url The URL associated with the request, for logging purposes. * @returns A promise that resolves to the HTML with inlined critical CSS. */ - private async inlineCriticalCss(html: string, url: URL, cache = false): Promise { - const { inlineCriticalCssProcessor, criticalCssLRUCache } = this; + private async inlineCriticalCss(html: string, url: URL): Promise { + const { inlineCriticalCssProcessor } = this; if (!inlineCriticalCssProcessor) { return html; } try { - if (!cache) { - return await inlineCriticalCssProcessor.process(html); - } - - const cacheKey = await sha256(html); - const cachedHtml = criticalCssLRUCache.get(cacheKey); - if (cachedHtml) { - return cachedHtml; - } - - const processedHtml = await inlineCriticalCssProcessor.process(html); - criticalCssLRUCache.put(cacheKey, processedHtml); - - return processedHtml; + return await inlineCriticalCssProcessor.process(html); } catch (error) { // eslint-disable-next-line no-console console.error(`An error occurred while inlining critical CSS for: ${url}.`, error); @@ -390,6 +376,41 @@ export class AngularServerApp { } } + /** + * Inlines critical CSS into the given HTML content. + * This method uses a cache to avoid reprocessing the same HTML content multiple times. + * + * @param html The HTML content to process. + * @param url The URL associated with the request, for logging purposes. + * @returns A promise that resolves to the HTML with inlined critical CSS. + */ + private async inlineCriticalCssWithCache( + html: string, + url: URL, + ): Promise> { + const { inlineCriticalCssProcessor, criticalCssLRUCache, textDecoder } = this; + + if (!inlineCriticalCssProcessor) { + return textDecoder.encode(html); + } + + const cacheKey = url.toString(); + const cached = criticalCssLRUCache.get(cacheKey); + const shaOfContentPreInlinedCss = await sha256(html); + if (cached?.shaOfContentPreInlinedCss === shaOfContentPreInlinedCss) { + return cached.contentWithCriticialCSS; + } + + const processedHtml = await this.inlineCriticalCss(html, url); + const finalHtml = textDecoder.encode(processedHtml); + criticalCssLRUCache.put(cacheKey, { + shaOfContentPreInlinedCss, + contentWithCriticialCSS: finalHtml, + }); + + return finalHtml; + } + /** * Constructs the asset path on the server based on the provided HTTP request. *