Stay updated

What lies behind rendering in a web browser
The change detection in Angular
Wednesday, November 11, 2020

When we talk about web solutions, change detection is a very important part of the framework that we are using because it’s responsible for the DOM updates. We must not forget that it also deeply affects an application’s performance.

Change detection has two main moments: tracking changes and rendering.

Let’s start from the rendering that is the process taking the internal state of the program (objects and arrays in Javascript) and projects it into something we can see on the screen(images, buttons and other visual elements).

At the beginning, the rendering logic implementation does not seem too difficult but everything gets complicated when we take into account the time variable. The application state can change any time as a result of a user interaction or because data have arrived after an HTTP call.


There is a very interesting example at this address written in pure Javascript for a rating widget.

Regardless of the initialisation code, we can write a class (let’s call it RatingComponent) containing a property called rating whose value changes on the user selection. This change must trigger a DOM update. The rating setter is the right moment to trigger by code the change of involved css classes.

export class RatingsComponent {
 set rating(v) {
 this._rating = v;
 // triggers DOM update
 get rating() {
 return this._rating;
 updateRatings() {
 // Update DOM code

This code does not scale with the growing complexity of the page and the conditional logic to be used in its elements. We must focus on the application logic and not on the screen updates.

That’s why we use a framework like Angular or libraries as React. Frameworks take care for us of synchronization between the internal state of the application and the user interfaces very efficiently.

How Angular implements the change detection?

When the compiler analyzes the template, it identifies component’s properties that are associated with DOM elements. For each association, the compiler creates a binding. A binding is a 1:1 correspondence between the involved property and the property of a DOM element.

Once bindings are created, Angular no longer works with templates.

The change detection mechanism executes instructions that process every binding. The job of these instructions is to check if the value of an expression with a component property has changed and perform DOM updates if necessary.

Let’s take as example a possible implementation of the rating component, seen before.

<ul class="rating" (click)="handleClick($event)">
    <li [className]="'star ' + (rating > 0 ? 'solid' : 'outline')"></li>
    <li [className]="'star ' + (rating > 1 ? 'solid' : 'outline')"></li>
    <li [className]="'star ' + (rating > 2 ? 'solid' : 'outline')"></li>
    <li [className]="'star ' + (rating > 3 ? 'solid' : 'outline')"></li>
    <li [className]="'star ' + (rating > 4 ? 'solid' : 'outline')"></li>

In this example, the rating property in the template is bound to the className property through the expression:

[className]="'star ' + ((ctx.rating > 0) ? 'solid' : 'outline')"

The first binding created will be an object with a current value (for example ‘outline’) and a property called dirty set to false. When the rating is updated, binding will be marked as dirty equal to true and its value set to ‘solid’. The next instruction will check if the binding is dirty and, in that case, it uses the new value to update the Dom (the className).

There are two ways to initiate the change detection. The first one is to invoke it manually from our code. The second one is to rely completely on the framework and let him run it automatically.

In the first case, we can inject the ChangeDetectorRef service (see here) in a component and call its detectChanges method in correspondence of an event.

In the second case, nothing is added to our code. The question is therefore: how does the framework know when it should run the change detection?

It’s easy to intercept an event and schedule a change detection after the application code has been executed. The problem, again, is the asynchronous events like setTimeout or XHR, which are not managed by Angular.

To solve this problem, the framework uses an external library called zone.js that creates a wrapper around all the asynchronous events in the browser. Then, zone.js may notify Angular when a particular event happens.

To make Zone available, you need to import the zone.js package. If you are using the command line interface, this step is done automatically, and you will see the following line in the src/polyfills.ts:

 * Zone JS is required by default for Angular itself.
 import 'zone.js/dist/zone'; // Included with Angular CLI.

It’s important to note that the zones are not part of the change detection mechanism in Angular. In fact, Angular can work without them as you can read here.

You just need to edit the main ts file responsible for the bootstrapping of our application

 .bootstrapModule(AppModule, {
 ngZone: 'noop'

The zones are an execution context for asynchronous operations. They turn out to be really useful to manage errors and make profiling. But what does that exactly mean?

In order to understand the part of the definition related to the execution context, we need to get a better picture of what problem the zones are trying to solve.

Let’s take as example 3 functions, executed in sequence:

function functionA() {...}
function functionB() {...}
function functionC() {...}

Let’s say we want to measure the execution time of this code.

const t0 = performance.now();
const t1 = performance.now();
console.log(Math.floor((t1-t0)*100) / 100 + ' ms');
function functionA() { console.log('function A');}
function functionB() { console.log('function B');}
function functionC() { console.log('function C');}

However, often we have asynchronous work to do. Asynchronous operations are ignored by our profile. For example:

const t0 = performance.now();
const t1 = performance.now();
console.log(Math.floor((t1-t0)*100) / 100 + ' ms');
function functionA() { setTimeout(doSomething, 0);}
function functionB() { setTimeout(doSomething, 0);}
function functionC() { setTimeout(doSomething, 0);}
function doSomething() {
 console.log('Async task');

results in:

What happened with our code? Javascript is a single-threaded language. The asynchronous behaviour it’s not part of the language itself but was added to it in the browser. The asynchronous features are accessible through the so called browser API.


Objects are allocated in a Heap which is just a name to denote a large (mostly unstructured) region of memory, as shown in the image.

The Stack is the single thread that executes the Javascript code. Function calls forms a stack of frames. What is a frame? Let’s come back to our example in the synchronous case and modify it slightly

function functionA(x) {
 let y = 3;
 return functionB(x * y);
function functionB(b) {
 let a = 10;
 return a + b + 11;

When calling functionA, a first frame is created containing functionA’s arguments and local variables. When functionA calls functionB, a second frame is created and pushed on top of the first one containing functionB’s arguments and local variables. When functionB returns, the top frame element is popped out of the stack (leaving only functionA’s call frame). When functionA returns, the stack is empty.

But what is the Queue shown in the picture? Let’s take this example introducing a setTimeout call.

function main() {
 setTimeout( function exec() {
 }, 0);
// Output
// A
// C
// B

setTimeout() has a delay of 0 ms and a callback function called exec(). setTimeout is pushed in the stack and its execution starts. But setTimeout belongs to the browser API then the responsibility to start the exec() function is delegated to the browser APIs. Meanwhile, setTimeout() is removed from the stack.

After 0 ms wait, browser APIs cannot push the exec function directly to the stack. What they can do is put the exec() function in a message queue.

Only when the stack is empty (frameless), the first item queued in the Message Queue can be pushed to the stack.

Meanwhile, in the stack was pushed the console.log(‘C’) instruction. This print will precede the one produced by exec():

The 0 that is in setTimeout() often causes confusion. It is never 0 but rather the minimum waiting time after which the callback function will run.

If we insert a blocking statement, the emptying of the queue will be further delayed.

I recommend you take a look at this web page containing a fantastic visual representation of what we have just explained. It is also possible to modify the code.

Let’s go back to the problem of profiling our code in presence of asynchronous code. What we need are basically hooks that allow us to execute some profiling code whenever such an asynchronous task happens.

This is where zones come into play. Zones can perform an operation (for example, starting or stopping a timer, or saving a stack trace) whenever that code enters or exits a zone. They can override methods within our code, or even associate data with individual zones.

Coming back to Angular, when an application is bootstrapped a zone is created called Ngzone. This is the zone that the Angular application runs in. And the framework only gets notifications about events that occur inside this zone.

Angular provides an ngZone service that can be injected in our application and that can be used to fork the Angular zone if we want to execute code outside of it.

More generally, the NgZone service provides us with a number of Observables and methods for determining the state of Angular’s zone and to execute code in different ways inside and outside Angular’s zone.

We can see a simple example where it’s necessary to use this service.

Let’s take the following code with its template.

export class AppComponent {
 get time() {
 return Date.now();
 Change detection is triggered at:
     <span [textContent]="time | date:'hh:mm:ss:SSS'"></span>
<button (click)="0">Trigger Change Detection</button>

This code generates an error in the console!

This is an error that has happened to anyone who has ever worked with Angular as you can read on the repository issues page on github. Although the long name explains exactly what happened the first reaction is definitely a desperate search on StackOverflow.

When Angular creates the DOM nodes to render the contents of the template of a component on the screen, it needs a place to store the references to those DOM nodes. For that purpose, internally there’s a data structure known as View. It’s also used to store the reference to the component instance and the previous values of binding expressions.

As said before, change detection is performed for each component. Now that we know that components are represented internally as views, we can say that the change detection is performed for each view.

When a view is checked, Angular runs over all bindings generated for a view by the compiler. It evaluates expressions and compares their result to the values stored in the oldValues array on the view. If it detects a difference, it updates the DOM property relevant to the binding and the oldValues array on the view.

After each change detection cycle, in development mode, Angular synchronously runs another check to ensure that expressions produce the same values as during the previous change detection cycle. This check doesn’t update the DOM but throws our nice exception.

Why execute a new cycle of checks?

Well, imagine that some properties of components have been updated during the change detection execution. As a result, expressions produce new values that are inconsistent with what’s rendered on the screen. Angular certainly could run another change detection cycle but it’s evident it could actually end up in an infinite loop of change detection runs.

To avoid this situation, Angular imposed the so-called Unidirectional Data Flow. Once Angular has processed bindings for the current component, you can no longer update its properties that are used in expressions for bindings. The raising of the ExpressionChangedAfterItHasBeenCheckedError exception is the mechanism to implement the data flow.

Ok, we just need another setInterval to trigger a new change detection cycle. Wrong! Again we will produce an infinite cycle of change detections.

The solution consists in executing the update of the time property in a zone different from the one of Angular. How can we make it? The first idea that comes to mind is using a setinterval in this zone.

export class AppComponent {
 mytime: number;
 get time(): number {
 return this.mytime;
constructor(private zone: NgZone) {
 this.mytime = Date.now();
 this.zone.runOutsideAngular(() => {
 setInterval(() => {
 this.mytime = Date.now();
 }, 0);

Please remember that the use of setinterval has to be deprecated in the real code you bring to production.

We can choose instead to use the onPush strategy on the component (ChangeDetectionStrategy.onPush), to prevent Angular from choosing when to run the change detection.

With onPush the change detection will kicks in when:

  1. The Input reference changes;
  2. An event originated from the component or one of its children;
  3. Run change detection explicitly (componentRef.markForCheck());
  4. Use the async pipe in the view.

I hope that you have been interested in this subject and that it has shed some light on an extremely complex subject.

See you next!

Written by

Salvatore Sorrentino