Deep Understanding

of

Change Detection

Stepan Suvorov

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