Skip to content

Commit a4c9a20

Browse files
committed
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 (cherry picked from commit afa2738)
1 parent 0b5cef0 commit a4c9a20

File tree

1 file changed

+49
-28
lines changed
  • packages/angular/ssr/src

1 file changed

+49
-28
lines changed

packages/angular/ssr/src/app.ts

Lines changed: 49 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -143,13 +143,15 @@ export class AngularServerApp {
143143
private readonly textDecoder = new TextEncoder();
144144

145145
/**
146-
* Cache for storing critical CSS for pages.
147-
* Stores a maximum of MAX_INLINE_CSS_CACHE_ENTRIES entries.
146+
* A cache that stores critical CSS to avoid re-processing for every request, improving performance.
147+
* This cache uses a Least Recently Used (LRU) eviction policy.
148148
*
149-
* Uses an LRU (Least Recently Used) eviction policy, meaning that when the cache is full,
150-
* the least recently accessed page's critical CSS will be removed to make space for new entries.
149+
* @see {@link MAX_INLINE_CSS_CACHE_ENTRIES} for the maximum number of entries this cache can hold.
151150
*/
152-
private readonly criticalCssLRUCache = new LRUCache<string, string>(MAX_INLINE_CSS_CACHE_ENTRIES);
151+
private readonly criticalCssLRUCache = new LRUCache<
152+
string,
153+
{ shaOfContentPreInlinedCss: string; contentWithCriticialCSS: Uint8Array<ArrayBufferLike> }
154+
>(MAX_INLINE_CSS_CACHE_ENTRIES);
153155

154156
/**
155157
* Handles an incoming HTTP request by serving prerendered content, performing server-side rendering,
@@ -198,7 +200,6 @@ export class AngularServerApp {
198200
*
199201
* @param request - The incoming HTTP request for serving a static page.
200202
* @param matchedRoute - The metadata of the matched route for rendering.
201-
* If not provided, the method attempts to find a matching route based on the request URL.
202203
* @returns A promise that resolves to a `Response` object if the prerendered page is found, or `null`.
203204
*/
204205
private async handleServe(
@@ -247,7 +248,6 @@ export class AngularServerApp {
247248
*
248249
* @param request - The incoming HTTP request to be processed.
249250
* @param matchedRoute - The metadata of the matched route for rendering.
250-
* If not provided, the method attempts to find a matching route based on the request URL.
251251
* @param requestContext - Optional additional context for rendering, such as request metadata.
252252
*
253253
* @returns A promise that resolves to the rendered response, or null if no matching route is found.
@@ -343,8 +343,8 @@ export class AngularServerApp {
343343
const stream = new ReadableStream({
344344
start: async (controller) => {
345345
const renderedHtml = await result.content();
346-
const finalHtml = await this.inlineCriticalCss(renderedHtml, url, true);
347-
controller.enqueue(this.textDecoder.encode(finalHtml));
346+
const finalHtml = await this.inlineCriticalCssWithCache(renderedHtml, url);
347+
controller.enqueue(finalHtml);
348348
controller.close();
349349
},
350350
});
@@ -355,33 +355,19 @@ export class AngularServerApp {
355355
/**
356356
* Inlines critical CSS into the given HTML content.
357357
*
358-
* @param html - The HTML content to process.
359-
* @param url - The URL associated with the request, for logging purposes.
360-
* @param cache - A flag to indicate if the result should be cached.
358+
* @param html The HTML content to process.
359+
* @param url The URL associated with the request, for logging purposes.
361360
* @returns A promise that resolves to the HTML with inlined critical CSS.
362361
*/
363-
private async inlineCriticalCss(html: string, url: URL, cache = false): Promise<string> {
364-
const { inlineCriticalCssProcessor, criticalCssLRUCache } = this;
362+
private async inlineCriticalCss(html: string, url: URL): Promise<string> {
363+
const { inlineCriticalCssProcessor } = this;
365364

366365
if (!inlineCriticalCssProcessor) {
367366
return html;
368367
}
369368

370369
try {
371-
if (!cache) {
372-
return await inlineCriticalCssProcessor.process(html);
373-
}
374-
375-
const cacheKey = await sha256(html);
376-
const cachedHtml = criticalCssLRUCache.get(cacheKey);
377-
if (cachedHtml) {
378-
return cachedHtml;
379-
}
380-
381-
const processedHtml = await inlineCriticalCssProcessor.process(html);
382-
criticalCssLRUCache.put(cacheKey, processedHtml);
383-
384-
return processedHtml;
370+
return await inlineCriticalCssProcessor.process(html);
385371
} catch (error) {
386372
// eslint-disable-next-line no-console
387373
console.error(`An error occurred while inlining critical CSS for: ${url}.`, error);
@@ -390,6 +376,41 @@ export class AngularServerApp {
390376
}
391377
}
392378

379+
/**
380+
* Inlines critical CSS into the given HTML content.
381+
* This method uses a cache to avoid reprocessing the same HTML content multiple times.
382+
*
383+
* @param html The HTML content to process.
384+
* @param url The URL associated with the request, for logging purposes.
385+
* @returns A promise that resolves to the HTML with inlined critical CSS.
386+
*/
387+
private async inlineCriticalCssWithCache(
388+
html: string,
389+
url: URL,
390+
): Promise<Uint8Array<ArrayBufferLike>> {
391+
const { inlineCriticalCssProcessor, criticalCssLRUCache, textDecoder } = this;
392+
393+
if (!inlineCriticalCssProcessor) {
394+
return textDecoder.encode(html);
395+
}
396+
397+
const cacheKey = url.toString();
398+
const cached = criticalCssLRUCache.get(cacheKey);
399+
const shaOfContentPreInlinedCss = await sha256(html);
400+
if (cached?.shaOfContentPreInlinedCss === shaOfContentPreInlinedCss) {
401+
return cached.contentWithCriticialCSS;
402+
}
403+
404+
const processedHtml = await this.inlineCriticalCss(html, url);
405+
const finalHtml = textDecoder.encode(processedHtml);
406+
criticalCssLRUCache.put(cacheKey, {
407+
shaOfContentPreInlinedCss,
408+
contentWithCriticialCSS: finalHtml,
409+
});
410+
411+
return finalHtml;
412+
}
413+
393414
/**
394415
* Constructs the asset path on the server based on the provided HTTP request.
395416
*

0 commit comments

Comments
 (0)