Change Detection can be defined as the process of identifying differences in the state of an object by observing it at different times. Each application is based on an internal state of objects that are displayed on the browser interface. The user can do a lot of actions that will cause changes in the internal state and it can be useful to track these changes. Change detection can also decrease the performance in big apps, if not handled correctly. As a developer, it’s good to know how these things work. So, let’s delve into this topic.


What are the goals of change detection?

  • Take the internal state of a program and make it visible to the user interface
  • Ensure that the user interface always stays in sync with the internal state of the program
  • Update the view (DOM) when the data has changed

image

Change detection is composed of two main parts:

  • Tracking changes - detecting that changes (may) have happened

  • Render the updated state on the screen - reacting to those potential changes


What are the things that cause change detection?

  • Events – click, submit, mouseover, keyup etc.
  • HTTP responses – fetching data from a remote server
  • Timers – setTimeout(), setInterval()
  • Event handlers - registered with addEventListener

In conclusion, any asynchronous operation.


How are asynchronous operations executed?

As we know, JavaScript is single threaded. Let’s dig a bit deeper into how asynchronous operations are run in JavaScript. JavaScript Runtime has one call stack and one memory heap.

  • Heap

    • where the memory allocation happens
    • when the variable loses its reference, the garbage collector will remove it from the heap
  • Stack

    • data storage which stores current function execution context of a program
    • basically records where in the program we are
    • will pop entries one by one as soon as that entry (function) returns some value, and it will continue pending function executions

Below we can see the execution of synchronous functions:

image

All synchronous code is executed directly on the main thread. Code execution will be done one at a time. Any code that takes longer to be executed will block the thread and the remaining tasks will stay pending. Maybe you saw this error in the browser:

image

image

Thread is responsible to handle everything: scrolling, printing something on the web page, listening to DOM events (like when user clicks a button), etc. The main purpose of asynchronous code is not to block the main thread. So, where are the asynchronous operations run? JavaScript runtime actually consists of 2 more components: event loop and calback queue which are responsible for async tasks execution.

image

As you see in the above image, async operations will be executed somewhere else. After an async task is finished, it will be pushed to the callback queue, which has the responsibility to keep all finished tasks. Also, we have the event loop that will take a task from callback queue and push it to stack when the stack is empty.​ ​

Where are the async tasks executed? What is that black box?image

JavaScript is run in the browser. Each browser has a Browser engine that is responsible to interpret the HTML, CSS, and JavaScript code of your website and present it in front of the audience. Browser engine is theimage of the browser and it is composed of:

  • Rendering Engine

    • responsible for the layout of the website on the audience’s screen
    • responsible for the paint and animations used on the website
  • JavaScript Engine

    • helps to interpret JavaScript code of the website before rendering it in front of the audience

    • a program or an interpreter which executes JavaScript code

    • can be implemented as a standard interpreter, or just-in-time compiler that compiles JavaScript to bytecode in some form

Browser engines are uniquely designed for every browser, and each of them has its own way of interpreting the web. I recommend reading more about browser engine.

image

Besides the main thread, each browser engine has other threads for handling important operations like compiling, code improvement, profiler, garbage collector and running the asynchronous operations image

image

As you see, the black box is Web Apis that are provided by the browser engine. Web Apis is a thread pool that gives additional functionality, including running asynchronous tasks in parallel. This is useful because it allows the user to continue using the browser normally while the asynchronous operations are being processed. Now, let’s analyze each part of the above schema.

  • Web Apis

    • Browser enhances the power of JavaScript by providing a collection of Web APIs
    • DOM events, Ajax are not executed in JavaScript runtime. Those are the tasks of web APIs, to be resolved by specific browsers.

    • When a task is done it will be pushed to the callback queue
    • They behave exactly like threads. The JavaScript accesses them and when a result comes back from the Web API, it will be handled on the main thread

    • Using them you can achieve true concurrency in JavaScript

  • Callback queue

    • A queue of callback functions that are waiting to be executed
    • The event loop will take the oldest task from the callback and will push it to the main thread when the stack is empty
    • In the modern JavaScript engine, the callback queue is divided into:
      • microtask queue - used for promises
      • macrotask queue - used for other async operations
    • Microtask queue has higher priority than macrotask queue. The event loop will pop a microtask instead of macrotask when the stack is empty

  • Event loop

    • Single-threaded loop that runs on the main JavaScript thread
    • Listens for the different events

    • Its job is to accept callback functions and execute them on the main thread
    • It only pushes a callback function to the stack when the stack is empty

    • If the main thread is busy, event loop is basically dead for that time

      while(queue.waitForMessage()){
          queue.processNextMessage();
      }
      

For a better understanding of asynchronous operations in JavaScript, I suggest the following:


Who notifies Angular?

Angular introduced ZoneJS to handle change detection. This library notifies angular when the UI should be rerendered. Usually, we don’t have to care about this - it just works. However, if something goes wrong with ZoneJS it can be very frustrating to analyze and understand what the problem is. This is why every developer should know what ZoneJS is and how the Angular uses it.

image

ZoneJS is an execution context for asynchronous operations. Basically, it patches all common async APIs (Events, Http responses, Timers, Event handlers, etc.) in order to keep track of them. For each async operation, ZoneJS creates a task that is run only in one zone. ZoneJS can perform an operation each time code enters or exits a zone. Angular has its own zone that is called NgZone. Tasks that are executed in the NgZone can cause a Change Detection. Several tasks can be executed simultaneously in the same zone. In Angular, almost all async operations will be executed in NgZone. It notifies Angular when all tasks are completed and it’s time to rerender the DOM.

Normally, a zone has these phases:

  • it starts stable

  • it becomes unstable if at least one task runs in the zone

  • it becomes stable again if all tasks are completed

export declare class NgZone {
    readonly hasPendingMacrotasks: boolean;
    readonly hasPendingMicrotasks: boolean;
    readonly isStable: boolean;
    
    readonly onUnstable: EventEmitter<any>;
    readonly onMicrotaskEmpty: EventEmitter<any>;
    readonly onStable: EventEmitter<any>;
    readonly onError: EventEmitter<any>;
    
    constructor({ enableLongStackTrace, shouldCoalesceEventChangeDetection }: {
        enableLongStackTrace?: boolean | undefined;
        shouldCoalesceEventChangeDetection?: boolean | undefined;
    });
    static isInAngularZone(): boolean;
    static assertInAngularZone(): void;
    static assertNotInAngularZone(): void;

    run<T>(fn: (...args: any[]) => T, applyThis?: any, applyArgs?: any[]): T;
    runTask<T>(fn: (...args: any[]) => T, applyThis?: any, applyArgs?: any[], name?: string): T;
    runGuarded<T>(fn: (...args: any[]) => T, applyThis?: any, applyArgs?: any[]): T;
    runOutsideAngular<T>(fn: (...args: any[]) => T): T;
}

NgZone has 4 events:

  • onUnstable - Notifies when code enters Angular Zone.
  • onMicrotaskEmpty - Notifies when there are no more microtasks enqueued in the current VM Turn. This is a hint for Angular to do the change detection.
  • onStable- Notifies when the last onMicrotaskEmpty was executed.
  • onError - Notifies when an error occured.

When is the change detection triggered?

Change detection is triggered when the last task was executed in Angular NgZone. In ApplicationRef we have a subscription to onMicrotaskEmpty event that will execute tick() method when the microtask queue becomes empty.

this._zone.onMicrotaskEmpty.subscribe({
    next: () => {
        this._zone.run(() => {
            this.tick();
        });
    }
});

Tick method is used to process change detection. It loops through the _views and calls their detectChanges method. Simply, NgZone extends Zone.js to include tick to run change detection.

tick() {
    if (this._runningTick) {
        throw new Error('ApplicationRef.tick is called recursively');
    }
    try {
        this._runningTick = true;
        for (let view of this._views) {
            view.detectChanges();
        }
        if (this._enforceNoNewChanges) {
            for (let view of this._views) {
                view.checkNoChanges();
            }
        }
    }
    catch (e) {
        this._zone.runOutsideAngular(() => this._exceptionHandler.handleError(e));
    }
    finally {
        this._runningTick = false;
    }
}

OnMicrotaskEmpty event is emitted fromcheckStable function

function checkStable(zone) {
    if (zone._nesting == 0 && !zone.hasPendingMicrotasks && !zone.isStable) {
        try {
            zone._nesting++;
            zone.onMicrotaskEmpty.emit(null);
        }
        finally {
            zone._nesting--;
            if (!zone.hasPendingMicrotasks) {
                try {
                    zone.runOutsideAngular(() => zone.onStable.emit(null));
                }
                finally {
                    zone.isStable = true;
                }
            }
        }
    }
}

ZoneJs has 4 hooks:

  • onInvokeTask - Triggers when an asynchronous task is about to execute, such as the callback of setTimeout().
  • onHasTask - Triggers when the status of a task inside a zone changes from stable to unstable or from unstable to stable. A status of stable means there are no tasks inside the zone, while unstable means a new task is scheduled in the zone.
  • onInvoke - Triggers when a synchronous function is going to be executed in the zone.

  • onHandleError - Triggers when an Error occurs.

With these hooks, Zone can monitor the status of all synchronous and asynchronous operations. Zone knows the moment a task enters or leaves the zone. These moments are handled by onEnter() and onLeave() functions.

function onEnter(zone) {
    zone._nesting++;
    if (zone.isStable) {
        zone.isStable = false;
        zone.onUnstable.emit(null);
    }
}

When a task enters the zone:

  • _nesting shows how many task are currently in the zone
  • the zone becomes unstable if currently it is stable
function onLeave(zone) {
    zone._nesting--;
    checkStable(zone);
}

When a task leaves the zone, the checkStable() function is called. If it was the last task, the zone becomes stable and angular will run the change detection.

By default, all asynchronous operations are executed inside the angular zone and this triggers change detection. Also, we can run async tasks outside the angular zone. NgZone has a method runOutsideAngular that runs code in NgZone’s _outer zone. Here no onMicrotaskEmpty event is emitted to trigger change detection. Any future tasks scheduled from this function will also be executed outside of the Angular zone.

constructor(private zone: NgZone) {
    this.zone.runOutsideAngular(() => {
        setTimeout(() => {
            // This code will not trigger change detection
        }, 3000);
    });
}

Zone helps Angular know when to trigger change detection. By default, Zone is loaded and works without additional configurations. However, you don’t necessarily have to use Zone to make Angular work. Instead, you can opt to trigger change detection on your own. To remove Zone.js, make the following changes.

  • Remove the zone.js import from polyfills.ts

  • Bootstrap Angular with the noop zone in src/main.ts

    platformBrowserDynamic().bootstrapModule(AppModule, {
        ngZone: 'noop'
    }).catch(err => console.error(err));
    

Angular provides a NoopNgZone. By using it, change detection will not be triggered automatically.

export declare class NoopNgZone implements NgZone {
    readonly hasPendingMicrotasks: boolean;
    readonly hasPendingMacrotasks: boolean;
    readonly isStable: boolean;
    readonly onUnstable: EventEmitter<any>;
    readonly onMicrotaskEmpty: EventEmitter<any>;
    readonly onStable: EventEmitter<any>;
    readonly onError: EventEmitter<any>;
    run(fn: (...args: any[]) => any, applyThis?: any, applyArgs?: any): any;
    runGuarded(fn: (...args: any[]) => any, applyThis?: any, applyArgs?: any): any;
    runOutsideAngular(fn: (...args: any[]) => any): any;
    runTask(fn: (...args: any[]) => any, applyThis?: any, applyArgs?: any, name?: string): any;
}

This can improve app performance, but it’s very possible that something will go wrong and the user will not see the synced data. You will have to decide yourself when the UI should be refreshed.


Change Detection

We already know what triggers change detection, but let’s see how it works in Angular. As we know, angular application consists of a components tree. Each component has its own change detector and this allows us to control each one individually. In the same way as components form a tree, there is a change detectors tree. It’s a directed graph where data always flows from top to bottom, meaning that the unidirectional data flow is more predictable than cycles.

image

Angular provides two strategies to run change detection: Default and OnPush

Default

This strategy is used by default, but you can set it in the component decorator metadata using ChangeDetectionStrategy.Default

@Component({
  ...
  changeDetection: ChangeDetectionStrategy.Default
})
export class MyComponent {

image

Using this strategy, angular will check every component in the components tree every time an event triggers change detection. Checking without making any assumption on the component’s dependencies is called dirty checking. It can negatively influence the performance in large apps. For each component that is checked, each expression that is used in the template will be compared with the previous value. If at least one property of the component is changed, the template will be rerendered.

OnPush

We can switch to the OnPush change detection strategy by setting the changeDetection property to ChangeDetectionStrategy.OnPush

@Component({
  ...
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyComponent {

image

This strategy reduces the number of components that will be checked. Using OnPush strategy, the component is updated only if:

  • The input reference has changed

    • Primitive types like numbers, string, booleans, null and undefined are passed by value

      @Input()
      title: string;
      

      Anytime title property changes its value, change detection will be triggered on this component and it will be rerendered.

    • Object and arrays are also passed by value, but modifying object properties or array entries does not create a new reference. To trigger change detection you need to pass a new object or array reference instead.

      Let’s suppose that we have an User class:

      class User {
        id: number;
        name: string;
          
        constructor(user: Partial<User>) {
          this.id = user.id;
          this.name = user.name;
        }
      }
      

      You pass User as an input:

      @Input()
      user: User;
      

      If you only update user properties from its parent component (user.name = 'John'), change detection will not be triggered. The reference will remain the same, so you should create a new one.

      user = new User({
          id: user.id,
          name: 'John'
      });
      

      If you have a list of users as an input:

      @Input()
      users: User[];
      

      If you want to add a new user to this list, the push method will not update the reference of the array and this will not trigger change detection.

      this.users.push(newUser);
      

      In order to rerender the component you should create a new list. The easiest solution is to use spread operator …

      this.users = [...users, newUser];
      
    • To prevent change detection bugs, I recommend:

      • building the application using OnPush strategy for all components
      • using immutable objects and lists. These can be only modified by creating a new object reference, so it is guaranteed that change detection will happen
  • The component or one of its children triggers an event handler

    Change detection will be triggered if the OnPush component or one of its child component triggers an event handler, like clicking on a button.

    Be careful, the following actions do not trigger change detection using the OnPush strategy:

    • setTimeout
    • setInterval
    • Promise.resolve().then(), (of course, the same for Promise.reject().then())
    • this.http.get(‘…’).subscribe() (in general, any RxJS observable subscription)
  • An observable linked to the template via the async pipe emits a new value

    The AsyncPipe automatically works using OnPush change detection strategy. Using async pipe makes OnPush easier to work with. Observables also give us some certain guarantees that a change has happened. Unlike immutable objects, observables don’t give us new references when a change is made. Instead, they fire events we can subscribe to in order to react to them. Internally the AsyncPipe calls markForCheck() each time a new value is emitted

      
    import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
    import { HttpClient } from '@angular/common/http';
    import { Observable } from 'rxjs';
    import { map, } from 'rxjs/operators';
      
    @Component({
      selector: 'app-my-component',
      template: `<ng-container *ngIf="user$ | async as user">
      				Name: 
      			</ng-container>`,  
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class MyComponent{
      user$: Observable<User>
      
      constructor(private http: HttpClient) {
          this.user$ = this.http.get('https://localhost/user/1');
      }
    }
    
  • Change detection is triggered manually

    You can do this using tick() method. For this you should inject ApplicationRef.

    constructor(private applicationRef: ApplicationRef) {
        // trigger change detection
        this.applicationRef.tick();
    }
    

    This triggers change detection for the whole application by respecting the change detection strategy of each component. You should take care when you use this method. There is a better way to trigger change detection without checking all components. Angular provides a ChangeDetectorRef class.

    export declare abstract class ChangeDetectorRef {
        abstract detach(): void;
        abstract reattach(): void; 
        abstract checkNoChanges(): void;
        abstract detectChanges(): void;
        abstract markForCheck(): void;
    }
    
    • detach - detaches this component and all descendants nodes from the change-detection tree. Detached views are not checked during change detection runs until they are re-attached, even if they are marked as dirty
    • reattach - re-attaches the previously detached view to the change detection tree in order to be checked during change detection runs
    • checkNoChanges - checks the change detector and its children, and throws if any changes are detected. It’s for development mode to verify that running change detection doesn’t introduce other changes
    • detectChanges - runs change detection on this view and its children by keeping the change detection strategy in mind.
    • markForCheck - does not trigger change detection but marks all OnPush ancestors as to be checked once, either as part of the current or next change detection cycle. It will run change detection on marked components even though they are using the OnPush strategy

    Running change detection manually is not a hack, but you should only use it in reasonable cases. If all components use Default change detection strategy, we should not care about running it manually.

Steps during change detection

A component instance has a lifecycle that starts when Angular instantiates the component and renders the component view along with its child views. The lifecycle continues with change detection, as Angular checks to see when data-bound properties change, updating the view and the component instance as needed. The lifecycle ends when Angular destroys the component instance and removes its rendered template from the DOM. I suggest reading more about lifecycle hook here.

image

As you can see, the AfterViewInit lifecycle hook is called after the DOM is updated and change detection was run. If you change the value in this hook, you will have a different value in the second change detection run (which is triggered automatically in the development mode, as described above) and therefore Angular will throw the ExpressionChangedAfterCheckedError. Let’s force this error with this example:

@Component({
  selector: 'app-my-example',
  template: `Time: `
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyExampleComponent {
    get time() {
        return Date.now();
    }
}

The app works, but if you inspect it you will see this error:

image

This can happen only on development mode, where the framework runs change detection twice to check if the value has changed since the first run. To have a better performance, change detection is only run once in the production mode. This error can occur very rarely, but if it happens, it can be very difficult to solve it.

Conclusion

It’s nice to have a good understanding of change detection in order not to spend a lot of time with:

  • solving a change detection error
  • understanding why a component is not refreshed
  • understanding why the performance is so low and how to fix this
  • finding out if change detection is run by Angular or if you should manually take care of this

Let’s finish this blog with an awesome example why this topic is so important. Let’s suppose we have an app with a grid containing several cells (all are angular components). Clicking on any cell changes the application state that is relevant (only) to that cell. An yellow border appears around the cell whenever Angular runs change detection on it.

Default

In the default change detection mode, Angular checks for changes for every component. As you can see in the below gif, clicking on any cell makes Angular run the change detection on all the cells.

image

OnPush

With OnPush change detection strategy, Angular will skip the change detection for a component as long as the references to the inputs do not change. As you can see in the below gif, clicking on a cell makes Angular run the change detection onlyfor that particular cell. Change detection checks were skipped entirely for all other cells.

image

Written by:
Dumitru.png
Dumitru Casap

Demystifying the empire to avoid the devils