We're experiencing a consistent 1-1.5 second delay in our Angular SSR v20.2.2 application that occurs after all Angular rendering completes but before the response is sent to the client. The delay appears to be caused by event loop blocking, but we cannot identify what's blocking it.
Environment
- Angular: 20.2.2 (using @angular/ssr with AngularNodeAppEngine)
- Node.js: v20.x
- Server: Express.js
- Platform: Hetzner Cloud CPX31 (4 vCPUs, 8GB RAM)
- SSR Setup: Standard Angular SSR with Apollo GraphQL client, TransferState enabled
- Payload Size: ~48KB compressed HTML
Diagnostic Code
We've instrumented our Express server with:
import { monitorEventLoopDelay } from 'node:perf_hooks';
import { createHook } from 'node:async_hooks';
// Monitor event loop delay
const eventLoopMonitor = monitorEventLoopDelay({ resolution: 20 });
eventLoopMonitor.enable();
// Track async operations
const asyncOperations = new Map();
const asyncHook = createHook({
init(asyncId, type) {
asyncOperations.set(asyncId, { type, startTime: Date.now() });
},
destroy(asyncId) {
asyncOperations.delete(asyncId);
},
});
asyncHook.enable();
// Express route with lifecycle tracking
server.get('*', (req, res, next) => {
const startTime = Date.now();
res.on('drain', () => {
console.log(`[SSR Lifecycle] 'drain' event at ${Date.now() - startTime}ms`);
});
res.on('finish', () => {
const { mean, stddev, max } = eventLoopMonitor;
console.log(`[Event Loop] Delay stats - mean: ${(mean/1e6).toFixed(2)}ms, max: ${(max/1e6).toFixed(2)}ms`);
});
// ... Angular SSR handling
});
Complete Log Output
2025-10-27T19:05:24.210+01:00[SSR] Starting request for: /
2025-10-27T19:05:24.210+01:00[SSR Perf] AngularNodeAppEngine created in 0ms
2025-10-27T19:05:24.210+01:00[SSR Perf] Gap before handle() starts: 0ms
2025-10-27T19:05:24.210+01:00[SSR Perf] AngularNodeAppEngine.handle() START for /
2025-10-27T19:05:24.212+01:00[SSR Perf] bootstrap() function CALLED
2025-10-27T19:05:24.212+01:00[SSR Perf] Angular bootstrap START (delay before start: 0ms)
2025-10-27T19:05:24.217+01:00[withDocumentMeta] initialized
2025-10-27T19:05:24.429+01:00[JpServerAuthService] autoLogin() parse "JPRefreshToken" cookie null
2025-10-27T19:05:24.434+01:00[SSR Perf] Angular bootstrap COMPLETE (222ms)
2025-10-27T19:05:24.524+01:00[SSR Perf] JpIndexPageComponent constructor START
2025-10-27T19:05:24.529+01:00[SSR Perf] JpIndexPageComponent constructor COMPLETE (6ms)
2025-10-27T19:05:24.535+01:00[Apollo] Starting operation: JpIndexPageArticlesQuery
2025-10-27T19:05:25.040+01:00[Apollo] Operation JpIndexPageArticlesQuery completed in 505ms
2025-10-27T19:05:25.324+01:00[SSR Perf] AngularNodeAppEngine.handle() COMPLETE (1114ms)
2025-10-27T19:05:25.324+01:00[SSR Perf] Total elapsed since request start: 1114ms
2025-10-27T19:05:25.325+01:00[SSR Perf] writeResponseToNodeResponse() returned after 0ms
2025-10-27T19:05:25.329+01:00[SSR Perf] TransferState serialization START
2025-10-27T19:05:25.329+01:00[SSR Perf] TransferState serialization COMPLETE (0ms, 47980 bytes)
// ⚠️ 1417ms GAP HERE - Event loop appears blocked
2025-10-27T19:05:26.741+01:00[SSR Lifecycle] 'drain' event at 2531ms
2025-10-27T19:05:26.743+01:00[SSR Lifecycle] 'finish' event (response sent) at 2533ms
2025-10-27T19:05:26.743+01:00[SSR] Request for / completed in 2533ms
2025-10-27T19:05:26.743+01:00[Event Loop] Delay stats - mean: 20.39ms, max: 1416.63ms, stddev: 14.38ms
2025-10-27T19:05:26.743+01:00[SSR Lifecycle] 'close' event (client disconnected) at 2533ms
2025-10-27T19:05:26.744+01:00[Event Loop] Delay stats - mean: NaNms, max: 0.00ms, stddev: NaNms
2025-10-27T19:05:26.744+01:00[Async Ops] 10 long-running operations detected:
2025-10-27T19:05:26.744+01:00 - TCPSERVERWRAP (5763417ms)
2025-10-27T19:05:26.744+01:00 - Timeout (5763416ms)
2025-10-27T19:05:26.744+01:00 - PROMISE (4982557ms)
2025-10-27T19:05:26.744+01:00 - PROMISE (4982557ms)
2025-10-27T19:05:26.744+01:00 - PROMISE (4982459ms)
2025-10-27T19:05:26.744+01:00 - PROMISE (4982458ms)
2025-10-27T19:05:26.744+01:00 - PROMISE (4982458ms)
2025-10-27T19:05:26.744+01:00 - PROMISE (4982458ms)
2025-10-27T19:05:26.744+01:00 - PROMISE (4982458ms)
2025-10-27T19:05:26.744+01:00 - PROMISE (4982458ms)
Our Hypothesis
The 1416ms max event loop delay suggests something is executing synchronous JavaScript for over 1 second, blocking Node.js from processing the I/O queue and writing the response to the socket.
Possible causes we're considering:
- Garbage collection pause (Node.js full GC)?
- Hidden synchronous DOM manipulation after rendering?
- Large JSON.stringify() operations we're not aware of?
- Third-party library doing synchronous work in post-render hooks?
- Angular SSR v20 internal operation we're missing?
Question
What could cause a consistent 1+ second event loop blocking delay in Angular SSR v20+ that occurs AFTER AngularNodeAppEngine.handle() completes and AFTER TransferState serialization, but BEFORE the response is written to the TCP socket?
Has anyone encountered similar post-render event loop blocking with AngularNodeAppEngine? Are there known Angular SSR v20+ operations that run synchronously after rendering completes?
We'd appreciate any insights from Node.js performance experts or Angular SSR developers who might have encountered this behavior!