Skip to main content

Change Detection

How it Works

Angular checks if component data has changed and updates the DOM accordingly.

@Component({
selector: 'app-user',
template: `
<p>{{ name }}</p>
<button (click)="changeName()">Change</button>
`
})
export class UserComponent {
name = 'John';

changeName() {
this.name = 'Jane'; // Triggers change detection
}
}

Default Strategy

import { ChangeDetectionStrategy } from '@angular/core';

@Component({
selector: 'app-default',
changeDetection: ChangeDetectionStrategy.Default, // default
template: `<p>{{ counter }}</p>`
})
export class DefaultComponent {
counter = 0;

// Change detection runs for:
// - Events (click, input, etc.)
// - HTTP requests
// - Timers (setTimeout, setInterval)
// - Any async operation
}

OnPush Strategy

import { Component, ChangeDetectionStrategy, input } from '@angular/core';

@Component({
selector: 'app-optimized',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<p>{{ user().name }}</p>
<p>{{ count }}</p>
`
})
export class OptimizedComponent {
// Signal-based input - only checks when INPUT reference changes
user = input.required<User>();
count = 0;

increment() {
this.count++; // Won't trigger CD with OnPush!
}
}

Triggering Change Detection with OnPush

1. Input Reference Change

// Parent component
users = [{ name: 'John' }];

updateUser() {
// ❌ Won't trigger CD
this.users[0].name = 'Jane';

// ✅ Will trigger CD
this.users = [{ name: 'Jane' }];

// ✅ Also works
this.users = [...this.users];
this.users[0] = { name: 'Jane' };
}

2. Events in Template

@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<p>{{ count }}</p>
<button (click)="increment()">+</button> <!-- Triggers CD -->
`
})
export class CounterComponent {
count = 0;

increment() {
this.count++; // Works because triggered by event
}
}

3. Async Pipe

@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div *ngFor="let user of users$ | async">
{{ user.name }}
</div>
`
})
export class UserListComponent {
users$ = this.userService.getUsers(); // Async pipe triggers CD
}

4. Manual with ChangeDetectorRef

import { ChangeDetectorRef } from '@angular/core';

@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ManualComponent {
cdr = inject(ChangeDetectorRef);
count = 0;

updateFromCallback() {
setTimeout(() => {
this.count++;
this.cdr.markForCheck(); // Mark for CD
}, 1000);
}

updateImmediate() {
this.count++;
this.cdr.detectChanges(); // Run CD immediately
}

ngOnDestroy() {
this.cdr.detach(); // Stop CD for this component
}
}

Signals and Change Detection

import { Component, signal, computed } from '@angular/core';

@Component({
selector: 'app-signal',
// OnPush works automatically with signals!
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<p>Count: {{ count() }}</p>
<p>Double: {{ double() }}</p>
<button (click)="increment()">+</button>
`
})
export class SignalComponent {
count = signal(0);
double = computed(() => this.count() * 2);

increment() {
this.count.update(v => v + 1); // Automatically triggers CD
}
}

Zone.js and Change Detection

import { NgZone } from '@angular/core';

@Component({})
export class ZoneComponent {
ngZone = inject(NgZone);

// Run outside Angular zone (no CD)
runOutsideAngular() {
this.ngZone.runOutsideAngular(() => {
setInterval(() => {
console.log('No CD triggered');
}, 1000);
});
}

// Run inside Angular zone (triggers CD)
runInsideAngular() {
this.ngZone.run(() => {
this.count++;
});
}
}

Detach/Reattach

import { ChangeDetectorRef } from '@angular/core';

@Component({})
export class DetachComponent {
cdr = inject(ChangeDetectorRef);

pauseCD() {
this.cdr.detach(); // Stop change detection
}

resumeCD() {
this.cdr.reattach(); // Resume change detection
}

manualCheck() {
this.cdr.detectChanges(); // Run CD once (even if detached)
}
}

Common Pitfalls

Mutating Arrays/Objects

// ❌ Won't work with OnPush
this.users.push(newUser);
this.user.name = 'Jane';

// ✅ Create new reference
this.users = [...this.users, newUser];
this.user = { ...this.user, name: 'Jane' };

Async Operations

// ❌ Won't trigger CD with OnPush
setTimeout(() => {
this.count++;
}, 1000);

// ✅ Use markForCheck
setTimeout(() => {
this.count++;
this.cdr.markForCheck();
}, 1000);

// ✅ Or use signals
timeout = signal(0);
setTimeout(() => {
this.timeout.update(v => v + 1);
}, 1000);

Performance Tips

@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- ❌ Function call every CD cycle -->
<p>{{ calculateTotal() }}</p>

<!-- ✅ Use computed/pipe -->
<p>{{ total }}</p>

<!-- ❌ Complex expression -->
<p>{{ users.filter(u => u.active).length }}</p>

<!-- ✅ Pre-calculate -->
<p>{{ activeUserCount }}</p>
`
})
export class PerformanceComponent {
users = signal([...]);

// Pre-calculate
activeUserCount = computed(() => {
return this.users().filter(u => u.active).length;
});
}

Best Practices

  • Always use OnPush for better performance
  • Use signals for reactive state (automatic CD)
  • Use async pipe for observables
  • Avoid mutating objects/arrays (use immutable updates)
  • Use trackBy with *ngFor
  • Minimize template expressions
  • Use computed for derived values
  • Detach CD for heavy components that update rarely
  • Profile with Chrome DevTools to find bottlenecks