Tuning Angular's Change Detection
In this article, you'll find a tutorial and video to help you grapple with one of the more advanced techniques in Angular, Change Detection.
Join the DZone community and get the full member experience.Join For Free
I recently prepared some course material for a modern web development workshop, which besides the nice features around ES6 obviously also included Angular. Change detection was part of that workshop and so I did some playing with it. Here are some things that you should know.
Contents are based on Angular version 2+.
I’m totally aware that there are already quite some good articles out there which go very deep into Angular’s change detection. Here are some I highly recommend to go through:
- Angular Change Detection Explained by Pascal Precht
- Essential Angular: Change Detection
- How I optimized Minesweeper using Angular 2 and Immutable.js to make it insanely fast
Especially Pascal gives a very good deep dive into the inner workings and some optimization strategies I was giving a look in this post here.
Table of Contents
- Change Detection in under 1 minute
- Tuning Change Detection
- Conclusion
Change Detection in Under 1 Minute
Before starting, did you ever wonder what “change detection” is about and why we actually need it? Tero Parviainen had the same question and wrote an in-depth article on the topic: Change And Its Detection In JavaScript Frameworks
In a nutshell, you usually have a model (i.e. a JavaScript object)…
const aPerson = { name: 'Juri', age: 31 }
…and some UI part (usually HTML) which visualizes the data of your model:
<p>Hi, my name is {{ aPerson.name }} and I'm {{ aPerson.age }} years old.</p>
Change detection is the process of keeping both in sync. Basically, when someone changes the name
aPerson.name, the HTML part should reflect that change.
Such changes come in through asynchronous things that might happen, such as
- Events (i.e. button clicks,..).
- XHR requests (i.e. calls to some API).
- Timers (i.e.
setTimeout,
setInterval).
Ok, so someone has to monitor all these asynchronous activities. But how? Exactly! While AngularJS (v1.x) did something called “dirty checking,” Angular (2+) monkey patches all these asynchronous parts using zones.js. Again, more details can be found in the articles I linked before.
(Ok that hopefully was a minute or so? )
Tuning Change Detection
While it is extremely fast, what’s important to remember, is that change detection has a cost.
"Change Detection has a cost."
Angular applies some very smart techniques to reduce this cost at a maximum. But still, whenever we can, we might want to help it a bit further. And we have lots of options to do so.
In order to understand these strategies, however, we need to know one thing first: the component tree.
As you might already know by now, every Angular application is structured as a tree of components. We have a top-level component, i.e. the
<app-root>, which contains other components, which again contain other components and so on (you get the idea ). In the end what we get is a nice, directed graph without cycles: a tree.
Within this tree structure, data flows “downwards” through properties, declared via
@Input and then back up from the child components to the parent component via events, using
@Output.
<app-root> / \ <contact> <favorites-list> / \ <person> <contact-details>
Whenever one of our asynchronous activities kicks in, say in the
<person> component, change detection is triggered from the root
<app-root> to the bottom. As a result, change detection is triggered on all of our components. Well, by now you might guess that in a big component tree, with lots of components, potentially also doing quite some heavy stuff, this might get an intense computation.
When we speak about optimizing the change detection, we really mean to cut off edges of our component tree, which the change detection process otherwise would have to traverse, thus it gets faster. Frankly, it’s like saying, hey, wait, we only changed
<person>and “I know” you don’t need to also verify
<favorites-list> etc.
So here are some strategies you can apply.
Immutable Objects
Before speaking about immutable objects, let’s see what happens “normally”. So we have an object, say..
@Component({..}) export class App { data = { counter: 0 }; }
..and this
data object is passed via an
@Input property to our child component
my-counter:
<my-counter [data]="data"></my-counter>
Whenever we change the
counter property of
data, change detection kicks in for the whole application (in our simple case, just for
<my-app> and
<my-counter>, but you get the point ).
With immutable objects, we try to optimize that verification step so that:
- Angular doesn’t have to perform a “deep” check of an object and all of its sub-objects.
- More importantly, by knowing the reference is different, and by knowing it can ignore any nested object of the
@Inputproperty, it can avoid checking that entire subtree of components.
This approach is called OnPush Change Detection and is activated on our
<my-counter>component as follows:
import {..., ChangeDetectionStrategy } from '@angular/core'; @Component({ ... changeDetection: ChangeDetectionStrategy.OnPush }) export class CounterComponent { @Input() data; }
After this change, simply modifying our
data.counter property, i.e. with
data.counter++, won’t trigger any change detection. Rather, we need to create a new instance:
this.data = { counter: ++this.data.counter };
Observables
If the immutable approach is not enough for you, we can even get better. Instead of passing in a data object, we pass in an
Observable.
@Component({ ... template: ` ... <my-counter [data]="data$"></my-counter> ` }) export class App { data$ = new BehaviorSubject({ counter: 0 }); ... }
In order to “broadcast” data on our
Observable we have to use its
next(...) function:
this.data$.next({ counter: ++this._counter });
Within our
<my-counter> component, we now have an
Observable<any> as the input type…
@Component({ ... changeDetection: ChangeDetectionStrategy.OnPush }) export class CounterComponent { @Input() data: Observable<any>; ... }
… and we obviously need to register ourselves on the
Observable to get notified in the event that new data is coming down the pipe. This is done using
subscribe(...).
export class CounterComponent { ... ngOnInit() { this.data.subscribe((value) => { // local variable bound to our template this._data = value; }); } }
These steps alone won’t work, however. Why? Well, we never change the reference of our Observable and since we still have
ChangeDetectionStrategy.OnPush, Angular’s won’t execute change detection on this sub-tree. Thus we need to manually mark the changes.
import {..., ChangeDetectorRef } from '@angular/core'; export class CounterComponent { constructor(private cd: ChangeDetectorRef) {} ... ngOnInit() { this.data.subscribe((value) => { this._data = value; // tell CD to verify this subtree this.cd.markForCheck(); }); } }
As you can see, whenever we get a new value in our
subscribe(..) we manually tell Angular to mark this sub-tree for change detection.
Manually Handling Change Detection
Well, if you’re still not satisfied, there’s one more option. You can even manually enable/disable change detection. This could turn out to be useful if you get lots and lots of data repeatedly from some backend and you want to perform change detection only every second or so.
In that case, the
ChangeDetectorRef has two interesting methods:
cd.detach()
cd.reattach()
Try it yourself in the runnable Plunker.
Conclusion
This is definitely part of the more advanced techniques when using Angular. But it is a powerful concept you should learn how to leverage to speed up your application even more.
I hope these runnable examples gave you some more insight (and a useful starting point) into the power of change detection in Angular. Definitely also check out Pascal’s article as well as his talk on change detection at NGNL2016.
Published at DZone with permission of Juri Strumpflohner. See the original article here.
Opinions expressed by DZone contributors are their own.