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.

References

Top comments (2)

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

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

Timeless DEV post...

Git Concepts I Wish I Knew Years Ago

The most used technology by developers is not Javascript.

It's not Python or HTML.

It hardly even gets mentioned in interviews or listed as a pre-requisite for jobs.

I'm talking about Git and version control of course.

One does not simply learn git