Table of Contents
- What is Change Detection?
- How Angular Tracks Changes
- The Change Detection Cycle
- Default Change Detection Strategy
- OnPush Change Detection Strategy
- Immutability: The Key to Efficient Change Detection
- Performance Optimization Tips
- Common Pitfalls and How to Avoid Them
- Conclusion
- References
1. What is Change Detection?
Change detection is the process by which Angular synchronizes the data in your components (the model) with the user interface (the view). Whenever the data in a component changes (e.g., a user clicks a button, an HTTP request returns data, or a timer elapses), Angular detects these changes and updates the DOM to reflect the new state.
Example: A Simple Counter
Consider a component with a counter that increments when a button is clicked:
// counter.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<h2>Count: {{ count }}</h2>
<button (click)="increment()">Increment</button>
`
})
export class CounterComponent {
count = 0;
increment() {
this.count++; // Data change
}
}
When the user clicks “Increment”, count increases. Angular detects this change and updates the {{ count }} binding in the template, refreshing the displayed value. This seamless synchronization is the work of change detection.
2. How Angular Tracks Changes
Angular needs to know when to run change detection. To do this, it leverages a library called Zone.js (via NgZone), which monkey-patches browser APIs (like setTimeout, fetch, and event listeners) to track asynchronous operations.
Key Concept: NgZone
NgZone creates a “zone” (a execution context) for Angular applications. Any asynchronous operation (e.g., click, setTimeout, HTTP request) that occurs within this zone triggers Angular to run change detection. This is why, in the counter example, clicking the button (an asynchronous event) automatically updates the view—NgZone detects the event and tells Angular to check for changes.
The Change Detector Tree
Angular creates a change detector for every component. These detectors form a tree mirroring the component tree. Each detector is responsible for checking changes in its component’s template bindings (e.g., {{ count }}, [input]="value").
3. The Change Detection Cycle
The change detection process follows a predictable sequence:
- An event occurs (e.g., user input, timer, HTTP response).
- NgZone detects the event and signals Angular to run change detection.
- Change detection runs from root to leaves: Angular traverses the component tree starting from the root component, checking each component’s change detector.
- Check for changes: For each component, the detector compares the current value of template expressions (e.g.,
{{ count }}) with their previous values. - Update the DOM: If a change is detected, the DOM is updated to reflect the new value.
- Cycle completes: The process finishes once all components are checked.
Key Note: Unidirectional Flow
Change detection in Angular is unidirectional: it flows from parent to child components. This prevents circular dependencies and makes the process predictable.
4. Default Change Detection Strategy
By default, Angular uses the ChangeDetectionStrategy.Default strategy. With this strategy:
- Every component is checked on every change detection cycle, regardless of whether its inputs or state have changed.
This is simple but can be inefficient for large apps with many components, as unnecessary checks waste CPU resources.
How Default Strategy Works
For a component using Default, Angular will:
- Check all template expressions (e.g.,
{{ user.name }},[disabled]="isDisabled"). - Check all input properties (
@Input()) for value changes (even if the reference is the same).
Example:
// user.component.ts
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-user',
template: `{{ user.name }}`,
changeDetection: ChangeDetectionStrategy.Default // Default (can be omitted)
})
export class UserComponent {
@Input() user: { name: string };
}
If the parent component passes user as { name: 'Alice' }, and later mutates it to { name: 'Bob' } (same reference), the Default strategy will detect the value change and update the view.
5. OnPush Change Detection Strategy
To optimize performance, Angular provides ChangeDetectionStrategy.OnPush, a more restrictive strategy that only runs change detection for a component under specific conditions:
When OnPush Triggers Change Detection
A component with OnPush will run change detection if:
- One of its input properties changes reference (not just value).
- An event occurs within the component (e.g.,
click,submit). ChangeDetectorRef.markForCheck()is called (manually trigger a check).ChangeDetectorRef.detectChanges()is called (force a check for this component and its children).
Key: Input Reference Changes
With OnPush, Angular only cares about reference changes for inputs, not value changes. If you mutate an input object (e.g., user.name = 'Bob'), Angular won’t detect the change because the object’s reference remains the same.
Example: OnPush in Action
// user.component.ts
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
@Component({
selector: 'app-user',
template: `{{ user.name }}`,
changeDetection: ChangeDetectionStrategy.OnPush // Use OnPush
})
export class UserComponent {
@Input() user: { name: string };
}
Parent Component:
// parent.component.ts
import { Component } from '@angular/core';
@Component({
template: `<app-user [user]="user"></app-user>`
})
export class ParentComponent {
user = { name: 'Alice' };
// Case 1: Mutate the user object (same reference)
mutateUser() {
this.user.name = 'Bob'; // OnPush: No change detected (reference unchanged)
}
// Case 2: Replace the user object (new reference)
replaceUser() {
this.user = { name: 'Bob' }; // OnPush: Change detected (new reference)
}
}
In mutateUser(), the user object’s reference doesn’t change, so OnPush doesn’t update the view. In replaceUser(), a new object is created (new reference), so OnPush triggers a check and updates the view.
6. Immutability: The Key to Efficient Change Detection
For OnPush to work effectively, you must use immutable data structures. Immutable data means you never mutate existing objects/arrays—instead, you create new ones when data changes.
Why Immutability Matters
- Predictable change detection: With immutability, a change in data is always signaled by a new reference, making it easy for
OnPushto detect. - Performance: Avoids unnecessary checks and makes change detection faster.
Examples of Immutability
Objects: Use Spread Operator or Object.assign
// Mutation (bad for OnPush)
this.user.name = 'Bob';
// Immutable update (good for OnPush)
this.user = { ...this.user, name: 'Bob' }; // New reference
Arrays: Use map, filter, or Spread Operator
// Mutation (bad)
this.items.push(newItem);
// Immutable update (good)
this.items = [...this.items, newItem]; // New array reference
7. Performance Optimization Tips
1. Use OnPush for Most Components
Adopt OnPush for components where inputs change infrequently or can be made immutable. This reduces the number of checks during each cycle.
2. Use Pure Pipes for Transformations
Pure pipes (the default) run only when their input references change. They are cached and avoid redundant computations.
// pure.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'uppercase' }) // Pure by default
export class UppercasePipe implements PipeTransform {
transform(value: string): string {
return value.toUpperCase();
}
}
3. Use trackBy with *ngFor
*ngFor re-renders all items by default when the array changes. trackBy lets you specify a unique identifier for items, so only changed items are re-rendered.
<!-- user-list.component.html -->
<ul>
<li *ngFor="let user of users; trackBy: trackByUserId">
{{ user.name }}
</li>
</ul>
// user-list.component.ts
trackByUserId(index: number, user: { id: number }): number {
return user.id; // Unique identifier
}
4. Detach/Reattach Change Detectors
For components that rarely change, detach their change detector to stop checks, then reattach when needed:
import { ChangeDetectorRef } from '@angular/core';
@Component({ ... })
export class StaticComponent {
constructor(private cdr: ChangeDetectorRef) {
this.cdr.detach(); // Stop change detection
}
updateData() {
this.data = newData;
this.cdr.reattach(); // Re-enable checks
this.cdr.detectChanges(); // Manually run check
}
}
5. Use markForCheck() for OnPush Components
If an OnPush component’s internal state changes (not via inputs), use markForCheck() to tell Angular to check it:
import { ChangeDetectorRef } from '@angular/core';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})
export class InternalStateComponent {
isUpdated = false;
constructor(private cdr: ChangeDetectorRef) {}
updateState() {
this.isUpdated = true;
this.cdr.markForCheck(); // Trigger change detection for this component
}
}
8. Common Pitfalls and How to Avoid Them
Pitfall 1: Mutating Data Instead of Replacing It
Problem: Mutating objects/arrays doesn’t trigger OnPush change detection.
Fix: Use immutable updates (spread operator, map, etc.).
Pitfall 2: Overusing Default Strategy
Problem: Unnecessary checks slow down the app.
Fix: Use OnPush for components with stable inputs.
Pitfall 3: Expensive Operations in Templates
Problem: Complex calculations in templates (e.g., {{ getTotal() }}) run on every change detection cycle.
Fix: Cache results or move logic to a pure pipe.
Pitfall 4: Forgetting markForCheck() with OnPush
Problem: Internal state changes in OnPush components don’t update the view.
Fix: Call this.cdr.markForCheck() after updating state.
9. Conclusion
Understanding Angular’s change detection is critical for building high-performance applications. By leveraging OnPush strategy, immutability, and optimization techniques like trackBy and pure pipes, you can ensure your app remains fast and responsive even as it scales.
Key takeaways:
- Change detection synchronizes the model and view.
NgZonetriggers change detection after async events.OnPushis optimal for most components and relies on immutable inputs.- Avoid mutations and use
markForCheck()/detectChanges()for manual control.
10. References
- Angular Official Documentation: Change Detection
- Zone.js Documentation
- Victor Savkin: Change Detection in Angular
- Max NgWizard: Angular Change Detection Explained
I hope this guide helps you master Angular change detection! Let me know in the comments if you have questions or tips to share. 🚀