Docs
/
Angular
Chapter 12
12 — Change Detection
How Change Detection Works
Angular checks the component tree to see if the DOM needs updating. By default, it checks every component from root to leaves on every event.
User clicks button
→ Zone.js patches the event
→ Angular triggers change detection
→ Checks AppComponent → checks all children → updates DOM
Change Detection Strategies
| Strategy | Behavior |
|---|---|
Default | Check this component on every CD cycle |
OnPush | Check only when inputs change, events fire, or signals update |
Default Strategy (Avoid in Production)
@Component({
changeDetection: ChangeDetectionStrategy.Default, // This is the default
})
- Checks on every browser event (click, scroll, timer, HTTP, etc.)
- Easy to use but slow in large apps
- Every component's template bindings are re-evaluated
OnPush Strategy (Recommended)
import { ChangeDetectionStrategy } from '@angular/core';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserListComponent {
@Input() users: User[] = [];
}
Angular only re-checks this component when:
- An
@Inputreference changes (not mutation!) - An event handler fires inside this component
- An
asyncpipe receives a new value - A signal used in the template updates
- Manual trigger via
ChangeDetectorRef
⚠️ Common Pitfall — Mutation vs Immutable
// ❌ Mutating the array — OnPush won't detect this
this.users.push(newUser);
// ✅ Create a new array reference — OnPush detects this
this.users = [...this.users, newUser];
// ❌ Mutating an object
this.user.name = 'New Name';
// ✅ Create a new object reference
this.user = { ...this.user, name: 'New Name' };
Manual Change Detection
import { ChangeDetectorRef } from '@angular/core';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MyComponent {
private cdr = inject(ChangeDetectorRef);
// Mark this component for check in next CD cycle
triggerCheck() {
this.cdr.markForCheck();
}
// Run CD immediately for this component and children
forceDetect() {
this.cdr.detectChanges();
}
// Detach from CD tree (manual control)
detach() {
this.cdr.detach();
}
// Reattach to CD tree
reattach() {
this.cdr.reattach();
}
}
Signals + OnPush = Best Performance
Signals automatically notify Angular when values change — no Zone.js needed.
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<p>Count: {{ count() }}</p>
<p>Double: {{ double() }}</p>
<button (click)="increment()">+</button>
`,
})
export class CounterComponent {
count = signal(0);
double = computed(() => this.count() * 2);
increment() {
this.count.update(c => c + 1);
// Angular knows count changed → re-renders this component only
}
}
Zone.js — What It Does
Zone.js monkey-patches all async APIs to trigger change detection:
Patched by Zone.js:
- setTimeout / setInterval
- Promise.then
- addEventListener (click, input, etc.)
- XMLHttpRequest / fetch
- requestAnimationFrame
When any of these complete, Zone.js tells Angular: "Something happened — run change detection."
Zoneless Angular (Experimental — Angular 18+)
Remove Zone.js entirely — Angular only updates when signals change or events fire.
// app.config.ts
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideExperimentalZonelessChangeDetection(),
provideRouter(routes),
provideHttpClient(),
],
};
// angular.json — remove zone.js polyfill
"polyfills": [] // Remove "zone.js" from here
Requirements for Zoneless:
- ✅ All components use
OnPushor signals - ✅ No reliance on Zone.js for triggering CD
- ✅ Use
asyncpipe or signals for observables - ❌ Don't rely on
setTimeouttriggering CD automatically
Performance Patterns
1. trackBy / track in Loops
<!-- ❌ Without track — Angular re-creates all DOM nodes on change -->
@for (user of users; track $index) { ... }
<!-- ✅ With unique ID — Angular reuses existing DOM nodes -->
@for (user of users; track user.id) { ... }
2. Pure Pipes Over Methods in Templates
<!-- ❌ Method called on EVERY CD cycle -->
<p>{{ calculateTotal() }}</p>
<!-- ✅ Pure pipe — only recalculates when input changes -->
<p>{{ items | total }}</p>
<!-- ✅ Computed signal — only recalculates when dependency changes -->
<p>{{ total() }}</p>
3. Detach Heavy Components
// For components that rarely change (e.g., static charts)
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class StaticChartComponent implements AfterViewInit {
private cdr = inject(ChangeDetectorRef);
ngAfterViewInit() {
this.renderChart();
this.cdr.detach(); // Stop checking this component entirely
}
// Manually trigger when data changes
updateChart(data: ChartData) {
this.data = data;
this.renderChart();
this.cdr.detectChanges();
}
}
Debugging Change Detection
// Enable Angular DevTools profiler (in Chrome DevTools)
// → Shows which components are checked and how long each takes
// Log when a component is checked
ngDoCheck() {
console.log('UserListComponent checked');
}
Key Takeaways
- Always use
ChangeDetectionStrategy.OnPush— it's a free performance win - With
OnPush, always use immutable data (spread operator, not mutation) - Signals work perfectly with
OnPushand are the path to zoneless Angular - Never call methods in templates for computed values — use
computed()or pure pipes - Use
track item.idin@forto prevent unnecessary DOM re-creation - Zoneless Angular (future default) requires signals throughout — start migrating now