Claude
Skills
Sign in
Back

change-detection

Included with Lifetime
$97 forever

Deep dive into Angular change detection optimization, OnPush strategy, Signals, and Zone.js patterns

General

What this skill does


# Angular Change Detection Optimization

Comprehensive guide to Angular change detection, OnPush strategy, Signals, and Zone.js optimization.

## Table of Contents

1. [How Change Detection Works](#how-change-detection-works)
2. [Default vs OnPush](#default-vs-onpush)
3. [OnPush Strategy Patterns](#onpush-strategy-patterns)
4. [Angular Signals](#angular-signals)
5. [Zone.js Optimization](#zonejs-optimization)
6. [Manual Change Detection](#manual-change-detection)
7. [Common Pitfalls](#common-pitfalls)
8. [Performance Monitoring](#performance-monitoring)

---

## How Change Detection Works

### The Basics

Angular uses **Zone.js** to detect async operations and trigger change detection:

```typescript
// Any of these trigger change detection:
setTimeout(() => {});           // ✓ Triggers CD
setInterval(() => {}, 1000);    // ✓ Triggers CD
fetch('/api/data');             // ✓ Triggers CD
button.addEventListener('click'); // ✓ Triggers CD
Promise.resolve();              // ✓ Triggers CD
```

### The Component Tree

```
        App Component
           /    \
          /      \
    Header      Main
                /  \
               /    \
          Sidebar  Content
                    /  \
                   /    \
              List     Detail
```

**Default Strategy**: When CD runs, Angular checks **every component** in the tree.

### Change Detection Cycle

1. **Event occurs** (click, HTTP response, timer)
2. **Zone.js intercepts** the async operation
3. **Angular runs change detection** from root
4. **Each component** checks if bindings changed
5. **DOM updates** if changes detected

---

## Default vs OnPush

### Default Strategy

```typescript
@Component({
  selector: 'app-user-list',
  template: `
    <div>{{ currentTime }}</div>
    <button (click)="refresh()">Refresh</button>
  `
})
export class UserListComponent {
  currentTime = new Date().toLocaleTimeString();
  
  refresh() {
    this.currentTime = new Date().toLocaleTimeString();
  }
}
```

**Behavior**:
- Checks on **every** CD cycle
- Even if data didn't change
- Performance cost: **High** (scales with components)

### OnPush Strategy

```typescript
@Component({
  selector: 'app-user-list',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div>{{ currentTime }}</div>
    <button (click)="refresh()">Refresh</button>
  `
})
export class UserListComponent {
  currentTime = new Date().toLocaleTimeString();
  
  refresh() {
    this.currentTime = new Date().toLocaleTimeString();
  }
}
```

**Behavior**:
- Only checks when:
  1. **@Input() reference changes**
  2. **Event handler** in component template
  3. **Observable emits** with async pipe
  4. **Manual detection** triggered
- Performance cost: **Low** (90% reduction)

### Performance Comparison

```typescript
// Scenario: 100 components, 10 CD cycles/second

// Default Strategy:
// 100 components × 10 cycles × 60 seconds = 60,000 checks/minute

// OnPush Strategy:
// ~5 components × 10 cycles × 60 seconds = 3,000 checks/minute
// ⚡ 95% reduction!
```

---

## OnPush Strategy Patterns

### Pattern 1: Immutable Data

```typescript
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductListComponent {
  @Input() products: Product[] = [];
  
  // ❌ BAD: Mutates array (OnPush won't detect)
  addProductBad(product: Product) {
    this.products.push(product);
  }
  
  // ✅ GOOD: New array reference
  addProductGood(product: Product) {
    this.products = [...this.products, product];
  }
  
  // ✅ GOOD: Immutable update
  updateProduct(id: string, updates: Partial<Product>) {
    this.products = this.products.map(p =>
      p.id === id ? { ...p, ...updates } : p
    );
  }
}
```

### Pattern 2: Async Pipe

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

### Pattern 3: Event Handlers

```typescript
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <button (click)="increment()">{{ count }}</button>
  `
})
export class CounterComponent {
  count = 0;
  
  // ✅ Event handlers in template trigger OnPush
  increment() {
    this.count++;
  }
}
```

### Pattern 4: Signals (Angular 16+)

```typescript
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div>{{ count() }}</div>
    <button (click)="increment()">+</button>
  `
})
export class CounterComponent {
  count = signal(0);
  
  increment() {
    this.count.update(c => c + 1);
  }
}
```

---

## Angular Signals

### Signal Basics

```typescript
// Create signal
const count = signal(0);

// Read signal
console.log(count()); // 0

// Update signal
count.set(5);
count.update(c => c + 1);

// Computed signal (derived state)
const doubled = computed(() => count() * 2);
```

### Signals in Components

```typescript
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <h2>Cart ({{ itemCount() }} items)</h2>
    <div>Total: {{ total() | currency }}</div>
    
    <div *ngFor="let item of items()">
      {{ item.name }} - {{ item.price | currency }}
      <button (click)="removeItem(item.id)">Remove</button>
    </div>
  `
})
export class CartComponent {
  items = signal<CartItem[]>([]);
  
  itemCount = computed(() => this.items().length);
  
  total = computed(() =>
    this.items().reduce((sum, item) => sum + item.price, 0)
  );
  
  addItem(item: CartItem) {
    this.items.update(items => [...items, item]);
  }
  
  removeItem(id: string) {
    this.items.update(items => items.filter(i => i.id !== id));
  }
}
```

### Signal Effects

```typescript
export class UserComponent {
  userId = signal<string | null>(null);
  user = signal<User | null>(null);
  
  constructor() {
    // Effect runs when dependencies change
    effect(() => {
      const id = this.userId();
      if (id) {
        this.userService.getUser(id).subscribe(
          user => this.user.set(user)
        );
      }
    });
  }
}
```

### Signals vs Observables

```typescript
// Observable approach
export class ProductsComponent {
  private searchTerm$ = new BehaviorSubject('');
  
  products$ = this.searchTerm$.pipe(
    debounceTime(300),
    switchMap(term => this.productService.search(term))
  );
  
  search(term: string) {
    this.searchTerm$.next(term);
  }
}

// Signal approach
export class ProductsComponent {
  searchTerm = signal('');
  
  products = computed(() => {
    // Note: computed is synchronous
    // For async, combine with effect
    return this.productService.search(this.searchTerm());
  });
  
  search(term: string) {
    this.searchTerm.set(term);
  }
}

// Hybrid approach (best for now)
export class ProductsComponent {
  searchTerm = signal('');
  
  products$ = toObservable(this.searchTerm).pipe(
    debounceTime(300),
    switchMap(term => this.productService.search(term))
  );
}
```

---

## Zone.js Optimization

### Run Outside Zone

```typescript
export class ChartComponent {
  constructor(private ngZone: NgZone) {}
  
  startAnimation() {
    // Run outside Angular's zone (no CD triggered)
    this.ngZone.runOutsideAngular(() => {
      const animate = () => {
        // Heavy animation logic
        this.updateChart();
        requestAnimationFrame(animate);
      };
      requestAnimationFrame(animate);
    });
  }
  
  // Manually trigger CD when needed
  updateData(data: any) {
    this.ngZone.run(() => {
      this.chartData = data;
    });
  }
}
```

### Polling Outside Zone

```typescript
export class LiveDataComponent implements OnInit, OnDestroy {
  data: any;
  private interval: any;
  
  constructor(private ngZone: NgZone) {}
  
  ngOnInit() {
    // Poll every second without triggering CD
    this.ngZone.runOutsideAngular(() => {
      

Related in General