Deep Understanding
of
Change Detection
Stepan Suvorov
-
CTO @ Studytube
-
Google Developer Expert at Angular
-
Angular Kharkiv(Ukraine) Meetup organizer
-
Dutch Angular Group co-organizer
-
Angular Workshops on javascript.info
-
Angular Pro Screencast (angular.ninja)
Why Change Detection is important?
Performace!
Performace
- Network
- Bundle Size Optimization
- SSR: Scully vs Universal
- ServiceWorkes
- Runtime
- Zones
- Change Detection
- WebWorkers
Zones
zone.js
// zone.js simplified
//--------------------------------
setTimeout(_ => {
console.log('some action');
}, 3000);
zone.js
// zone.js simplified
const oldSetTimeout = setTimeout;
setTimeout = (handler, timer) => {
console.log('START');
oldSetTimeout(_ => {
handler();
console.log('FINISH');
}, timer);
}
//--------------------------------
setTimeout(_ => {
console.log('some action');
}, 3000);
NgZone
// packages/core/src/zone/ng_zone.ts#L304
// ngzone simplified
function onEnter() {
_nesting++;
}
function onLeave() {
_nesting--;
checkStable();
}
function checkStable() {
if (_nesting == 0 && !hasPendingMicrotasks) {
onMicrotaskEmpty.emit(null);
}
}
onMicrotaskEmpty
// packages/core/src/application_ref.ts#L601
// ApplicationRef simplified
this._zone.onMicrotaskEmpty.subscribe({
next: () => {
this._zone.run(() => {
this.tick();
});
}
});
tick() {
for (let view of this._views) {
view.detectChanges();
}
}
Run code outside Zones
constructor(ngZone: NgZone) {
ngZone.runOutsideAngular(() => {
this._increaseProgress(() => {
// reenter the Angular zone and display done
ngZone.run(() => {
console.log('Outside Done!');
});
});
});
}
CD without Zones
platformBrowserDynamic()
.bootstrapModule(AppModule, { ngZone: 'noop' })
.catch(err => console.error(err));
class AppComponent {
constructor(app: ApplicationRef) {
setInterval(_ => app.tick(), 100);
}
}
Zone patch
<script>
__Zone_disable_timers = true; // setTimeout/setInterval/setImmediate
__Zone_disable_XHR = true; // XMLHttpRequest
__Zone_disable_Error = true; // Error
__Zone_disable_on_property = true; // onProperty such as button.onclick
__Zone_disable_geolocation = true; // geolocation API
__Zone_disable_toString = true; // Function.prototype.toString
__Zone_disable_blocking = true; // alert/prompt/confirm
</script>
Change Detection
most part of illustrations for this topic were taking from @PascalPrecht slides
Each component has its own Change Detector
On Push
@Component({
template: '<user-card [data]="data"></user-card>'
})
class MyApp {
constructor() {
this.data = {
name: 'Jack',
email: 'jack@mail.com'
}
}
changeData() {
this.data.name = 'John';
}
}
MyApp
UserCard
<user-card [data]="data"></user-card>
MyApp
UserCard
<user-card [data]="data"></user-card>
data = { name: "John", email: "jack@mail.com" }
MyApp
UserCard
<user-card [data]="data"></user-card>
data = { name: "John", email: "jack@mail.com" }
oldData === newData
MyApp
UserCard
<user-card [data]="data"></user-card>
data = { name: "John", email: "jack@mail.com" }
oldData === newData
Reference is the same, but the property could've changed (mutable), so we need to check
Angular is conservative by default and checks every component every single time
IMMUTABLE OBJECTS
@Component({
template: '<user-card [data]="data"></user-card>'
})
class MyApp {
constructor() {
this.data = {
name: 'Jack',
email: 'jack@mail.com'
}
}
changeData() {
this.data = {...this.data, name: John};
}
}
@Component({
template: `<h1>{{data.name}}</h1>
<span>{{data.email}}</span>`
})
class UserCardComponent {
@Input() data;
}
@Component({
template: `<h1>{{data.name}}</h1>
<span>{{data.email}}</span>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
class UserCardComponent {
@Input() data;
}
MyApp
UserCard
<user-card [data]="data"></user-card>
data = { name : "Jack", email: "jack@mail.com" }
oldData === newData
What if there was no Input() change but we still need to run CD?
@Component({
template: `<h1>{{data.name}}</h1>
<span>{{data.email}}</span>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
class UserCardComponent {
@Input() data;
contructor(dataService: DataService) {
dataService.onChange.subscribe(data => {
this.data = data;
});
}
}
Input() did not change, CD propagation stops
@Component({
template: `<h1>{{data.name}}</h1>
<span>{{data.email}}</span>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
class UserCardComponent {
@Input() data;
contructor(dataService: DataService,
cd: ChangeDetectorRef) {
dataService.onChange.subscribe(data => {
this.data = data;
cd.markForCheck();
});
}
}
Mark path until root for check
Perform change detection as usual
Restore original state
cd.markForCheck()
vs
cd.detectChanges()
cd.detach()
/cd.reattach()
@Component({
...
})
class UserCardComponent {
contructor(cd: ChangeDetectorRef) {
cd.detach();
// start of a heavy operation
// for which we want to skip CD
// ...
// ...
cd.reattach();
}
}
Thank you for your attention.
Questions?
Angular Performance Tuning
By Stepan Suvorov
Angular Performance Tuning
- 991