The utility of zone.js is terrific, since it works automatically, but a seasoned software engineer knows this often comes at a price. This is especially true when the concept of data binding comes into play.
In this recipe, you'll learn how to execute outside the Angular zone and what benefits this affords you.
The code, links, and a live example related to this recipe are available at .
To compare execution in different contexts, create two buttons that run the same code in different zone contexts. The buttons should count to 100 with setTimeout
increments. Use the performance global to measure the time it takes:
[src/app/app.component.ts] import {Component, NgZone} from '@angular/core'; @Component({ selector: 'app-root', template: ` <button (click)="runInsideAngularZone()"> Run inside Angular zone </button> <button (click)="runOutsideAngularZone()"> Run outside Angular zone </button> ` }) export class AppComponent { progress: number = 0; startTime: number = 0; constructor(private zone: NgZone) {} runInsideAngularZone() { this.start(); this.step(() => this.finish('Inside Angular zone')); } runOutsideAngularZone() { this.start(); this.step(() => this.finish('Outside Angular zone')); } start() { this.progress = 0; this.startTime = performance.now(); } finish(location:string) { this.zone.run(() => { console.log(location); console.log('Took ' + (performance.now() - this.startTime) + 'ms'); }); } step(doneCallback: () => void) { if (++this.progress < 100) { setTimeout(() => { this.step(doneCallback); }, 10); } else { doneCallback(); } } }
At this point, the two buttons will behave identically, as both of them are being executed inside the Angular zone.
In order to execute outside the Angular zone, you'll need to use the runOutsideAngular()
method exposed by NgZone
:
runInsideAngularZone() { this.start(); this.step(() => this.finish('Inside Angular zone')); } runOutsideAngularZone() { this.start(); this.zone.runOutsideAngular(() => { this.step(() => this.finish('Outside Angular zone')); }); }
At this point, you can run both of them again side by side and verify that they still take (roughly) the same amount of time to execute. This should not surprise you, as they are still performing the same task. The inclusion of zone.js means that the browser APIs are shimmed outside Angular, so even running this outside the Angular zone means it is still running inside a zone.
In order to see a performance difference, you'll need to introduce some binding inside the template:
[src/app/app.component.ts] import {Component, NgZone} from '@angular/core'; @Component({ selector: 'app-root', template: ` <h3>Progress: {{progress}}%</h3> <button (click)="runInsideAngularZone()"> Run inside Angular zone </button> <button (click)="runOutsideAngularZone()"> Run outside Angular zone </button> ` }) export class AppComponent { ... }
Now you should begin to see a substantive difference between the runtimes of each button. This shows that when the overhead of bindings is causing the application to slow down, runOutsideAngular()
has the potential to yield surprisingly substantive performance optimizations.
When you examine the NgZone
source, you'll find that the "outer" zone is merely the topmost browser zone. Angular forks this zone upon initialization and builds upon it to yield the nice NgZone
service wrapper.
However, because zones do not discriminate in the realm of asynchronous callbacks and data binding, each invocation of the setTimeout
handler inside the Angular zone is recognized as an event that has implications on the template rendering process. In every invocation, Angular sees an update to the bound data following an asynchronous task, and proceeds to rerender the view. When this is done 100 times, it adds up to several hundred extra milliseconds of execution.
When this is run outside the Angular zone, Angular is no longer aware of the setTimeout
tasks that are being executed and so does not require a rerender. Upon the very final iteration though, invoke NgZone.run()
; this will cause the execution to rejoin the Angular zone. Angular sees the task and the modified data and updates the bindings accordingly; this time though, this is done only once.
In this recipe, the finish()
method invokes the run()
method for both the Angular zone and the non-Angular zone. When the zone that this is invoked upon is already the contextual zone in which the task is being executed, using run()
becomes redundant and is effectively a no-op.