Angular SSR v16: saying goodbye to a sneaky trick - macrotask wrapping for HTTP calls 👋
Krzysztof Platis
Posted on July 3, 2023
Since version 16, Angular SSR no longer counts outgoing HTTP requests as macrotasks in Zone.js
, but uses an internal http interceptor function instead. As a consequence, if you used a technique of tracking NgZone
macrotasks for debugging hanging SSR, it will no longer reveal long pending HTTP requests for you. But the good news is you can still implement yourself a custom HttpInterceptor
for counting pending requests.
Before Angular 16 - wrapping HTTP requests as macrotasks
Before version 16, Angular created in Zone.js
an artificial macrotask named ZoneMacroTaskWrapper.subscribe
for each outgoing HTTP request in the special HttpHandler. Angular completed manually such an artifical macrotask when the HTTP request completed. Thanks to this trick, Zone.js
treated every pending outgoing HTTP request as a macrotask, even though by nature HTTP requests are not JavaScript macrotasks.
The observable ApplicationRef.isStable
, which is used internally by Angular SSR to hold the serialization of HTML until the app is stable, depended directly on the NgZone.onStable
, which emitted only when there were no pending microtasks and macrotasks (so also when there were no pending outgoing HTTP requests).
Since Angular 16 - no more wrapping HTTP requests as macrotasks
Since version 16, Angular removed the trick of wrapping HTTP calls as Zone.js
macrotasks. And Angular came up with a new, complementary solution for waiting until HTTP requests complete. Now Angular tracks pending HTTP requests with a special new class InitialRenderPendingTasks
which counts every pending http call via a new Angular's internal http interceptor function.
Now ApplicationRef.isStable
takes into account not only the observable NgZone.onStable
, but also InitialRenderPendingTasks.hasPendingTasks
.
So the old behavior is preserved - SSR is waiting for outgoing HTTP requests to complete. But it's just achieved differently now, with the help of the new class InitialRenderPendingTasks
.
Side note: the new class InitialRenderPendingTasks
is used also by Angular Router to ensure waiting for the initial navigation to complete.
How can I track outgoing requests in SSR?
You can write a custom Angular HTTP_INTERCEPTOR
, which will track pending outgoing requests. The following is an example implementation of such a custom interceptor:
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpResponse } from '@angular/common/http';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
interface RequestDetails {
url: string;
requestTime: Date;
responseTime: Date | undefined;
}
@Injectable()
export class DebuggingInterceptor implements HttpInterceptor {
private requests: RequestDetails[] = [];
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
this.requests.push({
url: request.url,
requestTime: new Date()
responseTime: undefined
});
return next.handle(request).pipe(
tap((event: HttpEvent<any>) => {
if (event instanceof HttpResponse) {
requestDetails.responseTime = new Date();
}
}, (error) => {
requestDetails.responseTime = new Date();
})
);
}
}
Note: the above implementation is just an example and can be enhanced by adding more request's details, for example the response HTTP status.
Note 2: Such an interceptor can be also implemented when using Angular versions prior to 16.
ZoneJS still triggers change detection on HTTP request completion
Even though HTTP requests are no longer wrapped as artifical macrotasks in Angular's internals, the change detection in Angular app still will be triggered after an outgoing request completes. It's possioble, because Zone.js
tracks also native event listeners (including native event listeners responsible for outgoing http request) and then triggers NgZone.onLeave()
, which triggers an internal function checkStable()
, which triggers emission of NgZone.onMicrotaskEmpty
(btw. I wonder why it wasn't named NgZone.onTaskEmpty
, because it's not only about microtasks, but also event listeners), which triggers emission of ApplicationRef.tick()
, which performs the change detection.
If you really feel like buying me a coffee
... then feel free to do it. Many thanks! 🙌
Posted on July 3, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.