ANGULAR CHANGE DETECTION
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
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 serverTimers
– 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:
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:
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.
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?
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 the 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.
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
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
- Browser enhances the power of JavaScript by providing a collection of
-
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 promisesmacrotask queue
- used for other async operations
Microtask queue
has higher priority thanmacrotask queue
. Theevent loop
will pop amicrotask
instead ofmacrotask
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:
-
Watch this amazing video about What the heck is the event loop by Philip Roberts
- Try these 2 tools in order to see the execution process of async operations:
- Read these articles:
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.
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 ofsetTimeout()
.onHasTask
- Triggers when the status of a task inside a zone changes from stable to unstable or from unstable to stable. A status ofstable
means there are no tasks inside the zone, whileunstable
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 frompolyfills.ts
-
Bootstrap Angular with the
noop
zone insrc/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.
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 {
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 {
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
- building the application using
-
-
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 theAsyncPipe
callsmarkForCheck()
each time a new value is emittedimport { 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 injectApplicationRef
.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 dirtyreattach
- re-attaches the previously detached view to the change detection tree in order to be checked during change detection runscheckNoChanges
- 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 changesdetectChanges
- 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.
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:
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.
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 only
for that particular cell. Change detection checks were skipped entirely for all other cells.
Recommended articles
- The Last Guide For Angular Change Detection You will Ever Need
- Everything you need to know about change detection in Angular
- Angular Change Detection Explained
- Change Detection Explained
- Angular Change Detection - How Does It Really Work?
- A gentle introduction into change detection in Angular
- Angular Ivy change detection execution: are you prepared?
- How to improve performance of Angular application with Change Detection and NgZone