Do you still think that NgZone (zone.js) is required for change detection in Angular?
This article explains why and how Angular implements NgZone over zone.js library. Read to learn how the framework can be used completely without zone.js and when automatic change detection can fail.

Do you still think that NgZone (zone.js) is required for change detection in Angular?
This article explains why and how Angular implements NgZone over zone.js library. Read to learn how the framework can be used completely without zone.js and when automatic change detection can fail.


Most articles I have seen strongly associate Zone
(zone.js
) and NgZone
with change detection in Angular. And although they are definitely related, technically they are not part of one whole. Yes, Zone
and NgZone
is used to automatically trigger change detection as a result of async operations. But since change detection is a separate mechanism it can successfully work without Zone
and NgZone
. In the first chapter I will show how Angular can be used without zone.js
. Second part of the article explains how Angular and zone.js interact together through NgZone
. In the end I’ll also show why automatic change detection sometimes doesn’t work with 3rd party libraries like Google API Client Library (gapi).
I’ve written many in-depth articles on change detection in Angular and this one completes the picture. If you’re looking for a comprehensive overview of how change detection works I recommend reading all of them starting with this one These 5 articles will make you an Angular Change Detection expert. Please note that this article is not about Zones (zone.js), but about how Angular uses Zones in the implementation of NgZone and its relationship with change detection mechanism. To learn more about Zones read I reverse-engineered Zones (zone.js) and here is what I’ve found.
Using Angular without Zone (zone.js)Link to this section
To demonstrate that Angular can successfully work without Zone I was initially planning to provide a mock zone object that simply doesn’t do anything. But the upcoming version 5 of Angular simplified things for me. It now provides a way to use a noop Zone that doesn’t do anything through configuration.
To do that let’s first remove dependency on zone.js
. I’ll use stackblitz to demo the application and since it uses Angular-CLI I will remove the following import from polyfils.ts
file:
<>Copy* Zone JS is required by Angular itself. */ import 'zone.js/dist/zone'; // Included with Angular CLI.
After that I’ll configure Angular to use the noop Zone implementation like this:
<>CopyplatformBrowserDynamic() .bootstrapModule(AppModule, { ngZone: 'noop' });
If you now run the application you will see that change detection is fully operational and renders name
component property in the DOM.
Now if we update this property using setTimeout
:
<>Copyexport class AppComponent { name = 'Angular 4'; constructor() { setTimeout(() => { this.name = 'updated'; }, 1000); }
You can see that the change is not updated. And it’s expected since NgZone
is not used and hence change detection is not triggered automatically. Yet it still works fine if we trigger it manually. This can be done by injecting ApplicationRef and triggering tick
method to start change detection:
<>Copyexport class AppComponent { name = 'Angular 4'; constructor(app: ApplicationRef) { setTimeout(()=>{ this.name = 'updated'; app.tick(); }, 1000); }
Now you can see that the update is successfully rendered.
To summarize, the point of the above demonstration is to show you that zone.js
and NgZone
in particular are not part of change detection implementation. It’s a very convenient mechanism to trigger change detection automatically by calling app.tick()
instead of doing it manually at certain points. We will see in a minute what those points.
How NgZone uses ZonesLink to this section
In my previous article on Zone (zone.js) I described in depth the inner working and API that Zone provides. There I explained the core concepts of forking a zone and running a task in a particular zone. I’ll be referring to those concepts here.
I also demonstrated two capabilities that Zone provides — context propagation and outstanding asynchronous tasks tracking. Angular implements NgZone class that relies heavily on the tasks tracking mechanism.
NgZone is just a wrapper around a forked child zone:
<>Copyfunction forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) { zone._inner = zone._inner.fork({ name: 'angular', ...
The forked zone is kept in the _inner
property and is usually referred to as Angular zone. This is the zone that is used to run a callback when you execute NgZone.run()
:
<>Copyrun(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()
:
<>CopyrunOutsideAngular(fn) { return this._outer.run(fn); }
This method is often used to run performance heavy operations outside Angular zone to avoid constantly triggering change detection.
NgZone has isStable
property that denotes whether there are no outstanding micro or macro tasks. It also defines four events:
<>Copy+------------------+-----------------------------------------------+ | Event | Description | +------------------+-----------------------------------------------+ | onUnstable | Notifies when code enters Angular Zone. | | | This gets fired first on VM Turn. | | | | | onMicrotaskEmpty | Notifies when there is no more microtasks | | | enqueued in the current VM Turn. | | | This is a hint for Angular to do change | | | detection which may enqueue more microtasks. | | | For this reason this event can fire multiple | | | times per VM Turn. | | | | | onStable | Notifies when the last `onMicrotaskEmpty` has | | | run and there are no more microtasks, which | | | implies we are about to relinquish VM turn. | | | This event gets called just once. | | | | | onError | Notifies that an error has been delivered. | +------------------+-----------------------------------------------+
And Angular uses the onMicrotaskEmpty
event inside ApplicationRef to automatically trigger change detection:
<>Copythis._zone.onMicrotaskEmpty.subscribe( {next: () => { this._zone.run(() => { this.tick(); }); }});
If you remember from the previous section the tick()
method is used to run change detection for the application.
How NgZone implements onMicrotaskEmpty eventLink to this section
Now let’s see how NgZone
implements the onMicrotaskEmpty
event. The event is emitted from the checkStable function:
<>Copyfunction checkStable(zone: NgZonePrivate) { if (zone._nesting == 0 && !zone.hasPendingMicrotasks && !zone.isStable) { try { zone._nesting++; zone.onMicrotaskEmpty.emit(null); <-------------------
And this function is regularly triggered from three Zone hooks:
As explained in the article about Zones when the last two hooks are triggered there’s a possibility of a change in the microtask queue so Angular has to run the stable
check every time the hooks are triggered. The onHasTask
hook is also used to perform a check since it tracks entire queue changes.
Common pitfallsLink to this section
One of the most frequent questions on stackoveflow related to change detection is why sometimes when using 3rd party libraries the changes in a component are not applied. Here is an example of such question involving Google API Client Library (gapi). The common solution to such problems is to simply run a callback inside Angular zone like this:
<>Copygapi.load('auth2', () => { zone.run(() => { ...
However, an interesting question is why Zone doesn’t register the request which leads to not having a notification in one of the hooks? And without notification NgZone
doesn’t automatically trigger change detection.
To learn that I just dig into gapi
minified sources and found that it uses JSONP to make a network request. This approach doesn’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.
Here is the relevant snipped from gapi minimized version for the curious:
<>CopyJa = function(a) { var b = L.createElement(Z); b.setAttribute(“src”, a); a = Ia(); null !== a && b.setAttribute(“nonce”, a); b.async = “true”; (a = L.getElementsByTagName(Z)[0]) ? a.parentNode.insertBefore(b, a) : (L.head || L.body || L.documentElement).appendChild(b) }
The Z
variable is equal to "script"
and the parameter a
holds the request URL:
<>Copyhttps://apis.google.com/_.../cb=gapi.loaded_0
The last segment of the URL is gapi.loaded_0
global callback:
<>Copytypeof gapi.loaded_0 “function”
That's it. Thanks for reading!
Comments (0)
Be the first to leave a comment
About the author

Principal Engineer at kawa.ai.. Founder indepth.dev. Big fan of software engineering, Web Platform & JavaScript. Man of Science & Philosophy.

About the author
Max Koretskyi
Principal Engineer at kawa.ai.. Founder indepth.dev. Big fan of software engineering, Web Platform & JavaScript. Man of Science & Philosophy.
About the author

Principal Engineer at kawa.ai.. Founder indepth.dev. Big fan of software engineering, Web Platform & JavaScript. Man of Science & Philosophy.