From zone.js to zoneless Angular and back — how it all works

Post Editor

Explore the mechanism behind automatic change detection in Angular with zone.js and use cases when to jump in and out of Angular zone

5 min read
0 comments
post

From zone.js to zoneless Angular and back — how it all works

Explore the mechanism behind automatic change detection in Angular with zone.js and use cases when to jump in and out of Angular zone

post
post
5 min read
0 comments
0 comments
This article is an excerpt from my Angular Deep Dive course

Change detection (rendering) in Angular is usually triggered completely automatically as a result of async events in a browser. This is made possible by utilizing zones implemented by zone.js library. In general, zones provide a mechanism to intercept the scheduling and calling of asynchronous operations. Interceptor logic can execute additional code before or after the task and notify interesting parties about the event. These rules are defined individually for each zone when it’s being created.

Zones are composed in a hierarchical parent-child relationship. At the start the browser runs in a special root zone, which is configured to behave exactly like the platform, making any existing code which is not zone-aware behave as expected. Only one zone can be active at any given time, and this zone can be retrieved through Zone.current property:

Content imageContent image

If you’re interested to learn how to work with zone.js directly, check out this article.

Contrary to popular belief, zones are not part of the change detection mechanism in Angular. In fact, Angular can work without zones using change detection services. To enable automatic change detection, Angular implements NgZone service that forks a child zone and subscribes to notifications from this zone.

This zone is referred to as Angular zone and all application specific code is expected to run inside this zone. That’s because NgZone only gets notifications about events that occur inside this Angular zone and doesn’t get any notifications about events in other zones:

Content imageContent image

If you explore NgZone, you’ll see that the reference to the forked Angular zone is stored in the _inner property:

<>Copy
export class NgZone { constructor(...) { forkInnerZoneWithAngularBehavior(self); } } function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) { zone._inner = zone._inner.fork({ ... }); }

This is the zone that is used to run a callback when you execute NgZone.run():

<>Copy
export class NgZone { run(fn, applyThis, applyArgs) { return this._inner.run(fn, applyThis, applyArgs); } }

The current zone at the moment of forking the Angular zone is kept in the _outer property and is used to run a callback when you execute NgZone.runOutsideAngular():

<>Copy
export class NgZone { runOutsideAngular(fn) { return (this as any as NgZonePrivate)._outer.run(fn); } }

Most often this outer zone is the top-most “root” zone.

When Angular finished its bootstrapping, that’s the hierarchy of zones you get:

<>Copy
"root" "angular"

Which you can easily see for yourself by simply logging the corresponding properties:

<>Copy
export class AppComponent { constructor(zone: NgZone) { console.log((zone as any)._inner.name); // angular console.log((zone as any)._outer.name); // root } }

However, in the development mode, there’s also AsyncStackTaggingZone that sits in between root and angular zone like this:

<>Copy
"root" "AsyncStackTaggingZone" "angular"

In this case, NgZone instance keeps a reference to the AsyncStackTaggingZone in its _inner property. AsyncStackTaggingZone provides linked stack traces to show where the async operation is scheduled. For more details, refer to the article Better Angular Debugging with DevTools.

The last bit that we need to know is that Angular instantiates NgZone during the bootstrapping phase:

<>Copy
export class PlatformRef { bootstrapModuleFactory<M>(moduleFactory, options?) { const ngZone = getNgZone(options?.ngZone, getNgZoneOptions(options)); const providers: StaticProvider[] = [{provide: NgZone, useValue: ngZone}]; // all initialization logic runs inside Angular zone return ngZone.run(() => {...}); } }

Angular uses the onMicrotaskEmpty event inside ApplicationRef to automatically trigger change detection for the entire application:

<>Copy
@Injectable({providedIn: 'root'}) export class ApplicationRef { constructor( private _zone: NgZone, private _injector: EnvironmentInjector, private _exceptionHandler: ErrorHandler, ) { this._onMicrotaskEmptySubscription = this._zone.onMicrotaskEmpty.subscribe({ next: () => { this._zone.run(() => { this.tick(); }); } }); } }

Let’s now see how Angular can work without zones.

Zoneless application
Link to this section

To run an Angular application without zone.js we need to pass noop value into bootstrapModule function for ngZone parameter like this:

<>Copy
platformBrowserDynamic() .bootstrapModule(AppModule, { ngZone: 'noop' });

If we now run this simple application:

<>Copy
@Component({ selector: 'app-root', template: `{{time}}` }) export class AppComponent { time = Date.now(); }

we’ll see that change detection is fully operational and renders time value in the DOM.

However, if update the name property inside the callback for setTimeout:

<>Copy
@Component({ selector: 'app-root', template: `{{time}}` }) export class AppComponent { time = Date.now(); constructor() { setTimeout(() => { this.time = Date.now() }, 1000); } }

We’ll see that the change is not updated. This is expected behavior since there’s no Angular zone to notify Angular about the timeout event occurrence. What’s interesting that we can still inject NgZone into the constructor:

<>Copy
import { ɵNoopNgZone } from '@angular/core'; export class AppComponent { constructor(zone: NgZone) { console.log(zone instanceof ɵNoopNgZone); // true } }

But it’s an empty implementation of NgZone which does nothing. We could use a change detector service to manually run change detection:

<>Copy
@Component({ selector: 'app-root', template: `{{time}}` }) export class AppComponent { time = Date.now(); constructor(cdRef: ChangeDetectorRef) { setTimeout(() => { this.time = Date.now() }, 1000); cdRef.detectChanges(); } }

We’ll go over this service in detail in manual control chapter.

Running code inside Angular zone

You may find yourself in the situation where you have a function that somehow runs outside of Angular’s zone and you don’t get the beneif to automatic change detection. It’s a common scenario with a third party library doing its stuff unaware of Angular context.

Here is an example of such question involving Google API Client Library (gapi). The common culprit is using techniques like JSONP that don’t use common AJAX APIs like XMLHttpRequest or Fetch API which are patched and tracked by Zones. Instead, it creates a script tag with a source URL and defines a global callback to be triggered when the requested script with data is fetched from the server. This can’t be patched or detected by Zones and hence the frameworks remains oblivious to requests performed using this technique.

The common solution to such problems is to simply run a callback inside Angular zone like this. For example, for gapi we should do it like this:

<>Copy
// Load the JavaScript client library. gapi.load('client', ()=> { // Run initialization code INSIDE Angular zone NgZone.run(()=>{ // Initialize the JavaScript client library gapi.client.init({...}).then(function() { ... }); }); });

Another example is a component that emits notifications from a callback that runs outside the Angular zone:

<>Copy
@Component({ selector: 'n-cmp', template: '{{title}} <div><n1-cmp></n1-cmp></div>' }) export class N { title = 'N component'; emitter = new Subject(); constructor(zone: NgZone) { zone.runOutsideAngular(() => { setTimeout(() => { this.emitter.next(3); }, 1000); }); } }

If we simply subscribe to the changes in the child N1 component like this:

<>Copy
@Component({ selector: 'n1-cmp', template: '{{title}}, emitted value: {{value}}' }) export class N1 { title = 'Child of N'; value = 'nothing yet'; constructor(parent: N, zone: NgZone) { parent.emitter.subscribe((v: any) => { this.value = v; }); } }

we won’t see any updates on the screen, even though the this.value is updated to 3 after the parent emits it. To fix this, just like in the gapi example, we could run the callback in Angular zone:

<>Copy
@Component({...}) export class N1 { title = 'Child of N'; value = 'nothing yet'; constructor(parent: N, zone: NgZone) { parent.emitter.subscribe((v: any) => { zone.run(() => { this.value = v; }); }); } }

this fixes it.


For more indepth stuff like what you read above check out the course:

Content imageContent image

If you believe something important is missing here do let me know in the comments!

Content imageContent image

Comments (0)

Be the first to leave a comment

Share

About the author

author_image

Max is a self-taught software engineer that believes in fundamental knowledge and hardcore learning. He’s the founder of inDepth.dev community and one of the top users on StackOverflow (70k rep).

author_image

About the author

Max Koretskyi

Max is a self-taught software engineer that believes in fundamental knowledge and hardcore learning. He’s the founder of inDepth.dev community and one of the top users on StackOverflow (70k rep).

About the author

author_image

Max is a self-taught software engineer that believes in fundamental knowledge and hardcore learning. He’s the founder of inDepth.dev community and one of the top users on StackOverflow (70k rep).

Looking for a JS job?
Job logo
Sr Angular Developer

Purple Drive Technologies

Worldwide
Remote
$95k - $120k
Job logo
Angular Developer

Sun Cloud LLC

Worldwide
Remote
$100k - $125k
Job logo
Ionic/Angular Developer

Synchrony Systems

United States
Remote
$110k - $135k
Job logo
Lead Full Stack Developer (Angular, Java, Rest)

73rd Solution

Worldwide
Remote
$115k - $146k
More jobs

Featured articles

Angularpost
17 January 202323 min read
Improve page performance and LCP with NgOptimizedImage

Explore mechanisms of NgOptimizedImage directive to improve overall page performance, targeting especially the Largest Contentful Paint (LCP) metric from Core Web Vitals. Enhance pages, make the best user experience and improve the web.