Local and Zoneless Change Detection

Agenda

what & when ❓

βš–οΈ

how to measure

challenges🚩

πŸš€

Local πŸ”

zoneless

πŸš€

Pankaj P. Parkar

Principal Application Devloper @AON

  • Angular GDE

  • Ex- Microsoft MVP (2015-22)

  • @ngx-lib/multiselect πŸ“¦Β 

About Me!

@pankajparkar

Data / Model / State

Template

DOM

Application Architecture

{
  "user": {
    "firstName": "Pankaj",
    "lastName": "Parkar",
    "age": 27
  }
}
<div class="card">
  First Name: {{user.firstName}}
  Last Name: {{user.lastName}}
  Age: {{user.age}}
</div>
First Name: Pankaj
Last Name: Parkar
Age: 27

28

28

@pankajparkar

What is Change Detection?

In simple word, it synchronises the model changes to view.

export class AppComponent {
  title  = 'Angular'
}
<span>
  {{title}}
</span>
<button (click)="title='Changed'">
  Change Title
</button>

Change Title

Angular

Changed

Click

@pankajparkar

Β πŸ€” When change detection is triggered?

  • It triggers CD in the below cases
    • Browser handled events
    • Asynchronous task (like network call, timer, promise, rAF, etc)Β 

ZoneJS

(Default CD)

Β πŸ€”When change detection is triggered?

value = 'No value';
response: any;

http = inject(HttpClient);

// Make API call
networkCall() {
  this.http
    .get(
      'https://jsonplaceholder.typicode.com/todos/1')
    .subscribe(
      (response: any) => (this.response = response));
}
// work on task
task() {
  setTimeout(() => {
    this.value = 'Test';
  }, 2000);
}
// on button click
startEvent() {
  this.networkCall();
  this.task();
}
Task - {{value}}
<hr>
Response - {{ response | json }}
<hr>
<button (click)="startEvent()">
  Start Event
</button>

ChangeDetectionStrategy.Default

1

2

3

App

Child 1

Child 2

Parent

Grand Child

πŸ‘†πŸ»

ChangeDetectionStrategy.Default

How to detect / measure CD?

  • Angular DevTools
  • Monkey patch `ApplicationRef` tick method
  • console in getter function on template
constructor(appRef: ApplicationRef) {
  const originalTick = appRef.tick;
  appRef.tick = () => {
    originalTick.apply(appRef);
    console.log(`App Level CD - ${++this.cdCount}`);
  };
}
  • ngAfterViewChecked or ngDoCheck hook
get test() {
  console.log('Update Parent');
  return 'Test';
}
ngAfterViewChecked() {
  this.count++;
}

How many times CD is triggered?

value = 'No value';
// Make API call
networkCall() {
  this.http
    .get('https://xyz.com/todos/1')
    .subscribe(
      response => this.response = response);
}

// work on task
task() {
  setTimeout(() => {
    this.value = 'Test';
  }, 2000);
}

// on button click
startEvent() {
  this.networkCall();
  this.task();
}
Task - {{value}}
Response - {{ response | json }}
<hr>
<button (click)="startEvent()">
  Start Event
</button>

ChangeDetectionStrategy.OnPush

OnPush - works with

  • Browser Handled event (tick)

  • Async Pipe (markForCheck)

  • Manual CD run (detectChanges)

  • Input Property change (markForCheck)

@Component({...})
export class ParentComponent {
    count = 0;
    child1Data = { value: 10 };
    child2Message = 'Hello from Parent';
    cd = inject(ChangeDetectorRef);
    ngOnInit() {
        setInterval(() => {
            this.child1Data = { value: Math.random() };
            this.cd.detectChanges();
        }, 2000);

        setInterval(() => {
            this.child2Message = 'Child 2 Message Changed';
            this.cd.detectChanges();
        }, 4000);
    }

    ngDoCheck() { this.count++; }
}
<h2>Parent Component CD Counts: {{ count }}</h3>
<app-child1 [data]="child1Data" />
<app-child2 [message]="child2Message" />
<h3>Child Component 1</h3>
<h3>CD Counts: {{ count }}</h3>
<p>Data Value: {{ data?.value }}</p>
<h3>Child Component 2</h3>
<h3>CD Counts: {{ count }}</h3>
<p>Message: {{ message }}</p>
<app-grand-child />
<h3>CD Counts: {{ count }}</h3>
<p>Grand child</p>

App

Child 1

Child 2

Parent

Grand Child

πŸ‘†πŸ»

ChangeDetectionStrategy.OnPush

OnPush

Skipped

Ran CD

πŸ‘†πŸ» Click Event Child Comp (tick)

πŸ”’

πŸ”’

πŸ”’

πŸ”’

πŸ”’

πŸ”“

πŸ”“

πŸ”“

Property Binding Change

App

Child 1

Child 2

Parent

Grand Child

ChangeDetectionStrategy.OnPush

async pipe (markForCheck)

πŸ”’

πŸ”’

πŸ”’

πŸ”’

πŸ”“

πŸ”

πŸ”“

πŸ”“

Mark for check

OnPush

Skipped

Ran CD

πŸ”’

πŸ”“

Property Binding Change

🚨 Challenges

  • Global change detection, check each component for binding updates (expensive 🐒 )Β 

  • Predictability πŸ€”

  • component at 100th level down, triggers CD from root component ⬇️

Local Change Detection

aka. Component Level Change Detection

  • Provides fine grained control over CD

  • Signals + OnPush

  • Component Signal modifies run change detection

Local Change Detection

Works with OnPush

  • Browser Handled event

  • Async Pipe

  • Manual change detection run

  • Input Property change

  • Signal update + consumed on template

@Component({...})
export class SignalGrandChildComponent {
  count = 0;
  newSignal = signal(0);
  _lastValue = 0;

  ngAfterViewChecked() {
    this.count++;
  }

  ngOnInit() {
    setInterval(() => {
      ++this._lastValue;
      if (this._lastValue % 3 === 0) {
        return;
      }
      this.newSignal.set(this._lastValue);
    }, 1000);
  }
}
<h3>CD Counts: {{ count }}</h3>
<p>Grand child</p>

newSignal {{ newSignal() }}

App

Child 1

Child 2

Parent

Grand Child

ChangeDetectionStrategy.OnPush

signal change (markForCheck)

πŸ”’

πŸ”’

πŸ”’

πŸ”’

πŸ”“

πŸ”

πŸ”“

πŸ”“

Skip for refresh

Refresh view

OnPush

Skipped

Ran CD

πŸ”’

πŸ”“

Property Binding Change

Zoneless Change Detection

  • Completely get rid of zone.js

  • Change detection will be scheduled by new Schedular API

  • Will help to shave off ~15kb

@Component({...})
export class SignalGrandChildComponent {
  count = 0;
  newSignal = signal(0);
  _lastValue = 0;

  ngAfterViewChecked() {
    this.count++;
  }

  ngOnInit() {
    setInterval(() => {
      ++this._lastValue;
      if (this._lastValue % 3 !== 0) {
        return;
      }
      this.newSignal.set(this._lastValue);
    }, 1000);
  }
}
<h3>CD Counts: {{ count }}</h3>
<p>Grand child</p>

newSignal {{ newSignal() }}

Zoneless Change Detection steps

  • Remove zonejs library reference from angular.json

  • use provideZonelessChangeDetection

- 15kb

Takeaways

  • How Change detection works?

    • Default - Depends on Zonejs
    • OnPush - mark component dirty
  • Local Change Detection

    • ​OnPush + Signal change

  • Zoneless change detection

    • removal on zonejs

    • under the hood uses scheduler API

  • ​Future Signal Components

References

  • https://medium.com/ngconf/local-change-detection-in-angular-410d82b38664
  • https://justangular.com/blog/a-change-detection-zone-js-zoneless-local-change-detection-and-signals-story
  • https://v17.angular.io/guide/change-detection
  • https://medium.com/angular-in-depth/deep-dive-into-the-onpush-change-detection-strategy-in-angular-fab5e4da1d69
  • https://riegler.fr/blog/2023-11-02-v17-change-detection?source=post_page-----410d82b38664---------------------------------------
  • https://dev.to/this-is-angular/on-the-road-to-fine-grained-change-detection-2a14?source=post_page-----410d82b38664---------------------------------------
Made with Slides.com