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
trackBywith*ngFor - Minimize template expressions
- Use computed for derived values
- Detach CD for heavy components that update rarely
- Profile with Chrome DevTools to find bottlenecks