Skip to content

Commit 2806932

Browse files
committed
perf(@angular/ssr): optimize response times by introducing header flushing
Improves SSR performance by streaming the response before inlining critical CSS. This allows for earlier header flushing, reducing time to first byte (TTFB) and improving perceived load times.
1 parent 815c050 commit 2806932

File tree

2 files changed

+45
-20
lines changed

2 files changed

+45
-20
lines changed

packages/angular/ssr/node/src/response.ts

+4
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ export async function writeResponseToNodeResponse(
4444
}
4545
}
4646

47+
if ('flushHeaders' in destination) {
48+
destination.flushHeaders();
49+
}
50+
4751
if (!body) {
4852
destination.end();
4953

packages/angular/ssr/src/app.ts

+41-20
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,11 @@ export class AngularServerApp {
129129
*/
130130
private boostrap: AngularBootstrap | undefined;
131131

132+
/**
133+
* Decorder used to convert a string to a Uint8Array.
134+
*/
135+
private readonly textDecoder = new TextEncoder();
136+
132137
/**
133138
* Cache for storing critical CSS for pages.
134139
* Stores a maximum of MAX_INLINE_CSS_CACHE_ENTRIES entries.
@@ -318,30 +323,46 @@ export class AngularServerApp {
318323
SERVER_CONTEXT_VALUE[renderMode],
319324
);
320325

321-
if (inlineCriticalCss) {
322-
// Optionally inline critical CSS.
323-
this.inlineCriticalCssProcessor ??= new InlineCriticalCssProcessor((path: string) => {
324-
const fileName = path.split('/').pop() ?? path;
325-
326-
return this.assets.getServerAsset(fileName).text();
327-
});
326+
if (!inlineCriticalCss) {
327+
return new Response(html, responseInit);
328+
}
328329

329-
if (renderMode === RenderMode.Server) {
330-
// Only cache if we are running in SSR Mode.
331-
const cacheKey = await sha256(html);
332-
let htmlWithCriticalCss = this.criticalCssLRUCache.get(cacheKey);
333-
if (htmlWithCriticalCss === undefined) {
334-
htmlWithCriticalCss = await this.inlineCriticalCssProcessor.process(html);
335-
this.criticalCssLRUCache.put(cacheKey, htmlWithCriticalCss);
330+
this.inlineCriticalCssProcessor ??= new InlineCriticalCssProcessor((path: string) => {
331+
const fileName = path.split('/').pop() ?? path;
332+
333+
return this.assets.getServerAsset(fileName).text();
334+
});
335+
336+
const { inlineCriticalCssProcessor, criticalCssLRUCache, textDecoder } = this;
337+
338+
// Use a stream to send the response before inlining critical CSS, improving performance via header flushing.
339+
const stream = new ReadableStream({
340+
async start(controller) {
341+
let htmlWithCriticalCss;
342+
343+
try {
344+
if (renderMode === RenderMode.Server) {
345+
const cacheKey = await sha256(html);
346+
htmlWithCriticalCss = criticalCssLRUCache.get(cacheKey);
347+
if (!htmlWithCriticalCss) {
348+
htmlWithCriticalCss = await inlineCriticalCssProcessor.process(html);
349+
criticalCssLRUCache.put(cacheKey, htmlWithCriticalCss);
350+
}
351+
} else {
352+
htmlWithCriticalCss = await inlineCriticalCssProcessor.process(html);
353+
}
354+
} catch (error) {
355+
// eslint-disable-next-line no-console
356+
console.error(`An error occurred while inlining critical CSS for: ${url}.`, error);
336357
}
337358

338-
html = htmlWithCriticalCss;
339-
} else {
340-
html = await this.inlineCriticalCssProcessor.process(html);
341-
}
342-
}
359+
controller.enqueue(textDecoder.encode(htmlWithCriticalCss ?? html));
360+
361+
controller.close();
362+
},
363+
});
343364

344-
return new Response(html, responseInit);
365+
return new Response(stream, responseInit);
345366
}
346367

347368
/**

0 commit comments

Comments
 (0)