Angular Universal SSR (Server Side Rendering) hangs when some asynchronous task in our app is not completed (like a forever-ticking setInterval()
, recursively-called setTimeout()
or never-completed HTTP call to API). Since Zone.js tracks all async tasks inside the Angular app, we can use Zone.js to identify the exact lines in the code that introduced the forever pending task. All we need is to import the plugin zone.js/plugins/task-tracking
and after a few seconds look up the deep internal state of the Angular's NgZone
.
1. Import zone.js/plugins/task-tracking
In your app.module.ts
import the following Zone.js plugin:
// app.module.ts
import 'zone.js/plugins/task-tracking';
...
2. Look up the deep internal state of the Angular's NgZone
after a few seconds
Copy-paste the following constructor
to your AppModule
:
// app.module.ts
...
export class AppModule {
constructor(ngZone: NgZone) {
/**
* CONFIGURE how long to wait (in seconds)
* before the pending tasks are dumped to the console.
*/
const WAIT_SECONDS = 2;
console.log(
`β³ ... Wait ${WAIT_SECONDS} seconds to dump pending tasks ... β³`
);
// Run the debugging `setTimeout` code outside of
// the Angular Zone, so it's not considered as
// yet another pending Zone Task:
ngZone.runOutsideAngular(() => {
setTimeout(() => {
// Access the NgZone's internals - TaskTrackingZone:
const TaskTrackingZone = (ngZone as any)._inner
._parent._properties.TaskTrackingZone;
// Print to the console all pending tasks
// (micro tasks, macro tasks and event listeners):
console.debug('π Pending tasks in NgZone: π');
console.debug({
microTasks: TaskTrackingZone.getTasksFor('microTask'),
macroTasks: TaskTrackingZone.getTasksFor('macroTask'),
eventTasks: TaskTrackingZone.getTasksFor('eventTask'),
});
// Advice how to find the origin of Zone tasks:
console.debug(
`π For every pending Zone Task listed above investigate the stacktrace in the property 'creationLocation' π`
);
}, 1000 * WAIT_SECONDS);
});
}
}
3. Start your SSR server
Compile and run your SSR app, e.g. run yarn dev:ssr
(or npm dev:ssr
)
4. Start the rendering
Open a page in the browser (or via another terminal window with command curl http://localhost:4200
; note: port might be different than 4200 in your case).
5. Find out the origin of the pending async task(s)
After a while (e.g. 2 seconds), you should see the list of all pending Zone tasks printed to the console. Each ZoneTask
object contains a property creationLocation
which points to the exact line in the code which caused this async task.
Now open the file path listed at the bottom of the stack trace (e.g. Ctrl+click the path on Windows; or Commnad+click on Mac). Then you should see the exact faulty line in the compiled main.js
that introduced the long time pending task.
Real Example
For example, here's console output in the app I was debugging:
β³ ... Wait 2 seconds to dump pending tasks ... β³
π Pending tasks in NgZone: π
{
microTasks: [],
macroTasks: [
ZoneTask {
_zone: [Zone],
runCount: 0,
_zoneDelegates: [Array],
_state: 'scheduled',
type: 'macroTask',
source: 'setInterval',
data: [Object],
scheduleFn: [Function: scheduleTask],
cancelFn: [Function: clearTask],
callback: [Function: timer],
invoke: [Function (anonymous)],
creationLocation: Error: Task 'macroTask' from 'setInterval'.
at TaskTrackingZoneSpec.onScheduleTask (/Users/krzysztof/code/test-ng12-i18next/dist/test-ng12-i18next/server/main.js:177338:36)
at ZoneDelegate.scheduleTask (/Users/krzysztof/code/test-ng12-i18next/dist/test-ng12-i18next/server/main.js:174750:45)
at Object.onScheduleTask (/Users/krzysztof/code/test-ng12-i18next/dist/test-ng12-i18next/server/main.js:174648:25)
at ZoneDelegate.scheduleTask (/Users/krzysztof/code/test-ng12-i18next/dist/test-ng12-i18next/server/main.js:174750:45)
at Zone.scheduleTask (/Users/krzysztof/code/test-ng12-i18next/dist/test-ng12-i18next/server/main.js:174562:37)
at Zone.scheduleMacroTask (/Users/krzysztof/code/test-ng12-i18next/dist/test-ng12-i18next/server/main.js:174593:21)
at scheduleMacroTaskWithCurrentZone (/Users/krzysztof/code/test-ng12-i18next/dist/test-ng12-i18next/server/main.js:175151:25)
at /Users/krzysztof/code/test-ng12-i18next/dist/test-ng12-i18next/server/main.js:177066:22
at proto.<computed> (/Users/krzysztof/code/test-ng12-i18next/dist/test-ng12-i18next/server/main.js:175438:18)
πππππat Backend.init (/Users/krzysztof/code/test-ng12-i18next/dist/test-ng12-i18next/server/main.js:178558:9),
ref: [Function: bound ],
unref: [Function: bound ]
}
],
eventTasks: []
}
π For every pending Zone Task listed above investigate the stacktrace in the property 'creationLocation' π
The faulty line in my case was setInterval()
which was never disposed:
... and by the way it was coming from a 3rd party dependency package - i18next-http-backend
(see source code). Then I fixed the hanging render just by setting the option backend.reloadInterval
to false
in the options of i18next
.
Caveats
At the time of writing (2022-03-15, zone.js v0.11.5) there exists is a bug in TaskTrackingZone
. If the setInterval()
has a shorter periodic timer value (e.g. 1000ms) than our debugging script's delay time (e.g. 2000ms), then this setInterval
task won't be logged in the list of pending Zone's macrotasks! When the callback of setInverval(callback, ms)
was invoked for the first time, then the task was removed from the array of tracked tasks in TaskTrackingZone
. See source code of TaskTrackingZone.
To fix this bug locally, you would need to change this line in your node_modules node_modules/zone.js/fesm2015/task-tracking.js
:
- if (task.type === 'eventTask')
+ if (task.type === 'eventTask' || (task.data && task.data.isPeriodic))
Bonus: use handy lib ngx-zone-task-tracking
instead of above code snippets
To make our lives easier, I published the npm package ngx-zone-task-tracking which prints to the console with a delay all the pending NgZone macrotasks and by the way fixes locally the bug mentioned before in TaskTrackingZone
. All you need is to npm install ngx-zone-task-tracking
and import ZoneTaskTrackingModule.printWithDelay(2000)
in your app module:
import { ZoneTaskTrackingModule } from 'ngx-zone-task-tracking';
/* ... */
@NgModule({
imports: [
ZoneTaskTrackingModule.printWithDelay(/* e.g. */ 2000)
]
})
export class AppModule {}
Here's the live demo of ngx-zone-task-tracking
.
Conclusion
Our Angular applications run plenty of small async operations. When the Angular Universal SSR hangs, it might be not obvious which async task(s) is forever pending. Fortunately, with the help of the plugin zone.js/plugins/task-tracking
and checking the internal state of Angular's NgZone
we can locate faulty lines in the source code (of our own or of 3rd party package). Then we know where to fix the hanging SSR.
Update 2022-04-07
I've fixed the bug mentioned above directly in the Angular repo! π (for more, see the article "How I became the Angular contributor π"). Now, I'm waiting for the new patch version of zone.js
to be published to npm.
Update 2023-07-03
I've learned only recently in details, how Angular tracks pending HTTP requests under the hood and how it changed since version 16.
Before Angular 16
Before Angular 16, HTTP requests were wrapped by Angular as artificially-created macrotasks (even though HTTP requests are not JavaScript macrotasks by nature). Thanks to that, Zone.js
could keep track of pending HTTP calls and NgZone.onStable
could emit when all async timers and HTTP requests completed.
The artifical macrotask created by Angular had a fancy name 'ZoneMacroTaskWrapper.subscribe'
. Unfortunately, to my knowledge, when printing to the console such a macrotask, you cannot find it's origin by stracktrace, nor find any details of the original request.
Since Angular 16
Since Angular 16, Angular no longer uses the trick of wrapping HTTP requests as Zone.js
macrotasks. So you will no longer see any 'ZoneMacroTaskWrapper.subscribe'
when printing to console all pending macro tasks. To track pending HTTP calls you can write a custom HttpInterceptor
. To learn more, see the article: Angular SSR v16: saying goodbye to a sneaky trick - macrotask wrapping for HTTP calls π.
References
- source code of
@angular/core
:NgZone
setsTaskTrackingZone
as its_inner
zone - https://github.com/angular/angular/blob/215db7fbe6c91c43383a784b8d74c8063ce5c340/packages/core/src/zone/ng_zone.ts#L138-L140 - source code of
zone.js/plugins/task-tracking
:TaskTrackingZone
captures the stacktrace of each task - https://github.com/angular/angular/blob/d1ea1f4c7f3358b730b0d94e65b00bc28cae279c/packages/zone.js/lib/zone-spec/task-tracking.ts#L40
If you really feel like buying me a coffee
... then feel free to do it. Many thanks! π
Top comments (6)
Hey Krzysztof,
Thanks for your article and the package to find long running tasks.
My App shows one long running macro task, but the last stack entry is just one line of the angular server js file.
Any Input on how I could find the actual code causing the long running element?
'ZoneMacroTaskWrapper.subscribe'
represents a pending HTTP call, I can say for sure now. I've checked the source code of Angular in version 14 and 15.However since version 16 HTTP calls won't be tracked as macrotasks in
Zone.js
anymore and you'll need a custom HTTP INTERCEPTOR anyway for tracking HTTP calls.For more, see the new section of this blogpost, which I've just added: #Update 2023-07-03
thanks for updating your blogpost :)
Hi Jan. Thanks for your comment!
Hard to tell without digging into the code.
My blind guess is that it might be some long-pending http call (angular Http Client exposes http calls as observables that we subscribe to). To eliminate this potential cause, please make sure to timeout all your outgoing http calls after a few seconds, e.g. With writing custom http interceptor.
Thanks for this. It helped to debug one of complex issue.
wow, how did u dig this one up? you are a digger arn't you π I personally just build with no optimization and open the main.js while running the server, and start debugging the old fashioned way :)