Angular
10 / 10

Change Detection & Performance

Angular Change Detection & Performance

Understanding and optimizing change detection is crucial for building performant Angular applications. Angular uses Zone.js to detect changes and update the view.

How Change Detection Works

Change detection is the mechanism Angular uses to keep the component tree in sync with the data model. When an event occurs (user input, HTTP response, timer), Angular checks if any data changed and updates the DOM if necessary.

import { Component, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';

// Default strategy - checks entire tree
@Component({
  selector: 'app-default',
  template: `
    <h2>{{ title }}</h2>
    <p>Count: {{ count }}</p>
    <button (click)="increment()">Increment</button>
  `
  // changeDetection: ChangeDetectionStrategy.Default  (default)
})
export class DefaultComponent {
  title = 'Default Strategy';
  count = 0;
  
  increment() {
    this.count++;
    // Angular automatically detects this change
  }
}

// OnPush strategy - only checks when inputs change or events occur
@Component({
  selector: 'app-onpush',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <h2>{{ title }}</h2>
    <p>Count: {{ count }}</p>
    <button (click)="increment()">Increment</button>
    <button (click)="updateAsync()">Update Async</button>
  `
})
export class OnPushComponent {
  @Input() data: any;
  title = 'OnPush Strategy';
  count = 0;
  
  constructor(private cdr: ChangeDetectorRef) {}
  
  // Button click triggers change detection
  increment() {
    this.count++;
  }
  
  // Async operation - need to manually trigger
  updateAsync() {
    setTimeout(() => {
      this.count++;
      this.cdr.markForCheck(); // Mark for check
    }, 1000);
  }
  
  // Or detect changes immediately
  forceUpdate() {
    this.count++;
    this.cdr.detectChanges(); // Run change detection now
  }
}

Change Detection Strategies

// Default: Checks component and all children on every change
@Component({
  changeDetection: ChangeDetectionStrategy.Default
})

// OnPush: Only checks when:
// 1. Input reference changes
// 2. Event handler fires
// 3. Observable emits (with async pipe)
// 4. Manually triggered (markForCheck/detectChanges)
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush
})

// Example: OnPush component
@Component({
  selector: 'app-user-card',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div>
      <h3>{{ user.name }}</h3>
      <p>{{ user.email }}</p>
    </div>
  `
})
export class UserCardComponent {
  @Input() user!: User;
  
  // ❌ Won't trigger change detection
  updateWrong() {
    this.user.name = 'New Name';
    // Mutating input property - reference stays same
  }
  
  // ✅ Will trigger change detection
  updateCorrect() {
    this.user = { ...this.user, name: 'New Name' };
    // New reference created
  }
}

// Parent component
@Component({
  template: `<app-user-card [user]="currentUser"></app-user-card>`
})
export class ParentComponent {
  currentUser = { name: 'John', email: 'john@example.com' };
  
  // ❌ Won't trigger child change detection
  updateWrong() {
    this.currentUser.name = 'Jane';
  }
  
  // ✅ Will trigger child change detection
  updateCorrect() {
    this.currentUser = { ...this.currentUser, name: 'Jane' };
  }
}

Manual Change Detection Control

import { Component, ChangeDetectorRef, ApplicationRef } from '@angular/core';

@Component({
  selector: 'app-manual',
  template: `<p>{{ count }}</p>`
})
export class ManualComponent {
  count = 0;
  
  constructor(
    private cdr: ChangeDetectorRef,
    private appRef: ApplicationRef
  ) {}
  
  // Mark component and ancestors for check
  markForCheck() {
    this.count++;
    this.cdr.markForCheck();
  }
  
  // Run change detection immediately for this component
  detectChanges() {
    this.count++;
    this.cdr.detectChanges();
  }
  
  // Detach from change detection tree
  detach() {
    this.cdr.detach();
    // Component no longer checked automatically
  }
  
  // Reattach to change detection tree
  reattach() {
    this.cdr.reattach();
  }
  
  // Run change detection for entire application
  triggerAppCheck() {
    this.appRef.tick();
  }
}

Performance Optimization Techniques

TrackBy Function

@Component({
  template: `
    <!-- Without trackBy - recreates all DOM nodes on array change -->
    <div *ngFor="let item of items">
      {{ item.name }}
    </div>
    
    <!-- With trackBy - only updates changed items -->
    <div *ngFor="let item of items; trackBy: trackByFn">
      {{ item.name }}
    </div>
  `
})
export class ListComponent {
  items = [
    { id: 1, name: 'Item 1' },
    { id: 2, name: 'Item 2' },
    { id: 3, name: 'Item 3' }
  ];
  
  // TrackBy function
  trackByFn(index: number, item: any): any {
    return item.id; // Unique identifier
  }
  
  // Or track by index
  trackByIndex(index: number): number {
    return index;
  }
}

Pure Pipes for Expensive Operations

// Pure pipe - cached result
@Pipe({
  name: 'expensiveFilter',
  pure: true  // default
})
export class ExpensiveFilterPipe implements PipeTransform {
  transform(items: any[], searchText: string): any[] {
    console.log('Pipe running'); // Logs only when inputs change
    return items.filter(item => 
      item.name.toLowerCase().includes(searchText.toLowerCase())
    );
  }
}

// Usage
<div *ngFor="let item of items | expensiveFilter:searchText">
  {{ item.name }}
</div>

Lazy Loading & Code Splitting

// Lazy load modules
const routes: Routes = [
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
  },
  {
    path: 'users',
    loadComponent: () => import('./users/user-list.component').then(m => m.UserListComponent)
  }
];

// Preloading strategies
import { PreloadAllModules, RouterModule } from '@angular/router';

RouterModule.forRoot(routes, {
  preloadingStrategy: PreloadAllModules  // Preload all lazy routes
});

Virtual Scrolling

import { ScrollingModule } from '@angular/cdk/scrolling';

@Component({
  standalone: true,
  imports: [ScrollingModule],
  template: `
    <cdk-virtual-scroll-viewport itemSize="50" style="height: 500px">
      <div *cdkVirtualFor="let item of items" class="item">
        {{ item.name }}
      </div>
    </cdk-virtual-scroll-viewport>
  `
})
export class VirtualScrollComponent {
  items = Array.from({ length: 10000 }, (_, i) => ({
    id: i,
    name: `Item ${i}`
  }));
}

Zone.js and NgZone

import { Component, NgZone } from '@angular/core';

@Component({
  selector: 'app-zone-demo'
})
export class ZoneDemoComponent {
  constructor(private ngZone: NgZone) {}
  
  // Run outside Angular zone - no change detection
  performHeavyTask() {
    this.ngZone.runOutsideAngular(() => {
      // Heavy computation or animation
      setInterval(() => {
        // This won't trigger change detection
        console.log('Running outside zone');
      }, 100);
    });
  }
  
  // Run inside Angular zone - triggers change detection
  updateUI() {
    this.ngZone.run(() => {
      // Update component data
      this.count++;
    });
  }
  
  // Handle events outside zone
  setupEventListener() {
    this.ngZone.runOutsideAngular(() => {
      document.addEventListener('mousemove', (event) => {
        // High-frequency event - outside zone for performance
        // Update when needed
        if (/* some condition */) {
          this.ngZone.run(() => {
            // Update component state
          });
        }
      });
    });
  }
}

Performance Best Practices

  • Use OnPush strategy for components with input properties

  • Use trackBy for *ngFor to avoid unnecessary DOM updates

  • Use pure pipes for expensive transformations

  • Lazy load modules and use code splitting

  • Use virtual scrolling for large lists

  • Run heavy computations outside Angular zone

  • Use async pipe for observables (auto unsubscribe)

  • Detach from change detection for static content

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free