DEV Community

Krzysztof Platis
Krzysztof Platis

Posted on • Updated on

How to find out why Angular SSR hangs - track NgZone tasks 🐾

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';
...

Enter fullscreen mode Exit fullscreen mode

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);
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

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' πŸ‘†
Enter fullscreen mode Exit fullscreen mode

The faulty line in my case was setInterval() which was never disposed:
Image description
... 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))
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (5)

Collapse
 
janwidmer profile image
Jan Widmer

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?
Image description

Collapse
 
krisplatis profile image
Krzysztof Platis • Edited

'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

Collapse
 
janwidmer profile image
Jan Widmer

thanks for updating your blogpost :)

Collapse
 
krisplatis profile image
Krzysztof Platis

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.

Collapse
 
ayyash profile image
Ayyash

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 :)