Skip to main content

Overview

Angular’s change detection system is responsible for synchronizing your application’s data model with its view. Understanding how it works is crucial for building performant applications.

Change Detection Strategies

Angular provides two primary change detection strategies that control when and how components are checked for changes.

Default (Eager) Strategy

The Default strategy (also known as Eager) checks the component eagerly during every change detection cycle.
import { Component, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app-eager',
  template: `
    <h2>{{ title }}</h2>
    <p>Checked: {{ getCheckedCount() }}</p>
  `,
  changeDetection: ChangeDetectionStrategy.Default
})
export class EagerComponent {
  title = 'Eager Component';
  private checkedCount = 0;

  getCheckedCount(): number {
    return ++this.checkedCount;
  }
}
The Default strategy has been renamed to Eager in recent versions. Both names refer to the same behavior.

OnPush Strategy

The OnPush strategy only checks the component when:
  • An input property reference changes
  • An event originates from the component or its children
  • Change detection is manually triggered
  • An observable linked to the template emits a new value
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';

interface User {
  id: number;
  name: string;
  email: string;
}

@Component({
  selector: 'app-user-card',
  template: `
    <div class="card">
      <h3>{{ user.name }}</h3>
      <p>{{ user.email }}</p>
      <button (click)="onEdit()">Edit</button>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserCardComponent {
  @Input() user!: User;

  onEdit(): void {
    // Event handler triggers change detection
    console.log('Editing user:', this.user.id);
  }
}

Manual Change Detection

ChangeDetectorRef

Use ChangeDetectorRef to manually control change detection in your components.
import { 
  Component, 
  ChangeDetectionStrategy, 
  ChangeDetectorRef,
  OnInit,
  OnDestroy 
} from '@angular/core';
import { interval, Subscription } from 'rxjs';

@Component({
  selector: 'app-manual-cd',
  template: `
    <div class="status">
      <h3>Real-time Data</h3>
      <p>Counter: {{ counter }}</p>
      <p>Last Updated: {{ lastUpdate | date:'medium' }}</p>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ManualChangeDetectionComponent implements OnInit, OnDestroy {
  counter = 0;
  lastUpdate = new Date();
  private subscription?: Subscription;

  constructor(private cdr: ChangeDetectorRef) {}

  ngOnInit(): void {
    // Update data outside Angular's zone
    this.subscription = interval(1000).subscribe(() => {
      this.counter++;
      this.lastUpdate = new Date();
      
      // Manually trigger change detection
      this.cdr.markForCheck();
    });
  }

  ngOnDestroy(): void {
    this.subscription?.unsubscribe();
  }
}

Key Methods

// Marks the component and its ancestors for checking
// Use with OnPush strategy
this.cdr.markForCheck();

Performance Optimization

Immutable Data Patterns

When using OnPush, work with immutable data to ensure change detection triggers correctly.
import { Component, ChangeDetectionStrategy } from '@angular/core';

interface TodoItem {
  id: number;
  title: string;
  completed: boolean;
}

@Component({
  selector: 'app-todo-list',
  template: `
    <div *ngFor="let todo of todos; trackBy: trackByFn">
      <app-todo-item 
        [todo]="todo" 
        (toggle)="onToggle($event)"
      ></app-todo-item>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodoListComponent {
  todos: TodoItem[] = [
    { id: 1, title: 'Learn Angular', completed: false },
    { id: 2, title: 'Build an app', completed: false }
  ];

  onToggle(id: number): void {
    // Create new array reference for OnPush to detect change
    this.todos = this.todos.map(todo => 
      todo.id === id 
        ? { ...todo, completed: !todo.completed }
        : todo
    );
  }

  trackByFn(index: number, item: TodoItem): number {
    return item.id;
  }
}

Avoiding Common Pitfalls

Avoid calling functions or methods directly in templates with OnPush strategy, as they may not be re-evaluated when expected.
// ❌ Bad: Function calls in template
@Component({
  template: `<p>{{ getFormattedDate() }}</p>`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class BadComponent {
  getFormattedDate(): string {
    return new Date().toISOString();
  }
}

// ✅ Good: Use pipes or properties
@Component({
  template: `<p>{{ currentDate | date:'medium' }}</p>`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class GoodComponent {
  currentDate = new Date();
}

Change Detection with Signals

When using signals in your templates, Angular automatically tracks dependencies and triggers change detection efficiently.
import { Component, signal, computed, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app-counter',
  template: `
    <div class="counter">
      <p>Count: {{ count() }}</p>
      <p>Double: {{ doubled() }}</p>
      <button (click)="increment()">Increment</button>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
  count = signal(0);
  doubled = computed(() => this.count() * 2);

  increment(): void {
    this.count.update(n => n + 1);
  }
}
Signals work seamlessly with OnPush change detection, automatically marking components for check when signal values change.

Zone.js Integration

Angular uses Zone.js to automatically detect asynchronous operations and trigger change detection.
import { Component, NgZone } from '@angular/core';

@Component({
  selector: 'app-zone-aware',
  template: `<p>Data: {{ data }}</p>`
})
export class ZoneAwareComponent {
  data = 'initial';

  constructor(private ngZone: NgZone) {
    // Run code outside Angular's zone for performance
    this.ngZone.runOutsideAngular(() => {
      setInterval(() => {
        // Update data outside zone
        const newData = this.fetchData();
        
        // Run change detection when needed
        this.ngZone.run(() => {
          this.data = newData;
        });
      }, 5000);
    });
  }

  private fetchData(): string {
    return `Updated at ${new Date().toLocaleTimeString()}`;
  }
}

Best Practices

Use OnPush

Set OnPush strategy on presentational components to reduce unnecessary checks.

Immutable Data

Use immutable data patterns to make change detection reliable and predictable.

TrackBy Functions

Always use trackBy with *ngFor to minimize DOM operations.

Avoid Heavy Computations

Move expensive calculations to pipes or pre-compute them in component logic.

Additional Resources