Skip to content

Commit b6b765f

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 9bce4c4 commit b6b765f

File tree

2 files changed

+40
-20
lines changed

2 files changed

+40
-20
lines changed

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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -318,30 +318,46 @@ export class AngularServerApp {
318318
SERVER_CONTEXT_VALUE[renderMode],
319319
);
320320

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-
});
321+
if (!inlineCriticalCss) {
322+
return new Response(html, responseInit);
323+
}
328324

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);
325+
this.inlineCriticalCssProcessor ??= new InlineCriticalCssProcessor((path: string) => {
326+
const fileName = path.split('/').pop() ?? path;
327+
328+
return this.assets.getServerAsset(fileName).text();
329+
});
330+
331+
const { inlineCriticalCssProcessor, criticalCssLRUCache } = this;
332+
333+
// Use a stream to send the response before inlining critical CSS, improving performance via header flushing.
334+
const stream = new ReadableStream({
335+
async start(controller) {
336+
let htmlWithCriticalCss;
337+
338+
try {
339+
if (renderMode === RenderMode.Server) {
340+
const cacheKey = await sha256(html);
341+
htmlWithCriticalCss = criticalCssLRUCache.get(cacheKey);
342+
if (!htmlWithCriticalCss) {
343+
htmlWithCriticalCss = await inlineCriticalCssProcessor.process(html);
344+
criticalCssLRUCache.put(cacheKey, htmlWithCriticalCss);
345+
}
346+
} else {
347+
htmlWithCriticalCss = await inlineCriticalCssProcessor.process(html);
348+
}
349+
} catch (error) {
350+
// eslint-disable-next-line no-console
351+
console.error(`An error occurred while inlining critical CSS for: ${url}.`, error);
336352
}
337353

338-
html = htmlWithCriticalCss;
339-
} else {
340-
html = await this.inlineCriticalCssProcessor.process(html);
341-
}
342-
}
354+
controller.enqueue(htmlWithCriticalCss ?? html);
355+
356+
controller.close();
357+
},
358+
});
343359

344-
return new Response(html, responseInit);
360+
return new Response(stream, responseInit);
345361
}
346362

347363
/**

0 commit comments

Comments
 (0)