AngularJSChange Detection

Change Detection

Change detection (CD) is how Angular keeps the DOM in sync with your application's data. Angular checks whether bound values have changed and updates the view accordingly. Understanding CD is essential for building high-performance Angular applications — and for debugging mysterious "why isn't my template updating?" bugs.

How Change Detection Works

Angular runs change detection in response to async events:

  • User interactions (click, keypress, input)
  • HTTP responses
  • Timer callbacks (setTimeout, setInterval)
  • WebSocket messages
  • Promises resolving

These are tracked by Zone.js, which monkey-patches browser APIs to notify Angular when any async operation completes.

When Angular runs CD on a component, it:

  1. Evaluates all template expressions (bindings, pipes)
  2. Compares new values with the previous values
  3. If different, updates the DOM
  4. Runs CD on all child components
Note
Angular 18+ is moving toward a Zoneless future where Zone.js is optional, and signals drive CD automatically. The patterns on this page remain relevant during the transition.
Default Change Detection Strategy

By default, Angular uses ChangeDetectionStrategy.Default. On every CD cycle, Angular checks every component in the tree — from root to leaf.

TS
// Default strategy — checks on every CD cycle
@Component({
  selector: 'app-list-item',
  template: `<li>{{ item.name }}</li>`,
  // changeDetection: ChangeDetectionStrategy.Default  ← implicit default
})
export class ListItemComponent {
  @Input() item!: { name: string; active: boolean };
}

If you have 1000 components, Angular runs template evaluation for all 1000 on every button click. For most apps this is fine, but for data-heavy UIs it can cause jank.

OnPush Change Detection Strategy

ChangeDetectionStrategy.OnPush tells Angular: only check this component when one of these conditions is met:

  1. An @Input() reference changes (new object/array reference)
  2. An event is triggered from within this component (click, keyup, etc.)
  3. An async pipe emits a new value
  4. Change detection is triggered manually via ChangeDetectorRef
  5. A signal read in the template changes (Angular 17+)

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

@Component({
  selector: 'app-user-card',
  standalone: true,
  // Only re-render when inputs change or template signals emit
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="card">
      <h3>{{ user.name }}</h3>
      <p>{{ user.email }}</p>
    </div>
  `,
})
export class UserCardComponent {
  @Input() user!: User;
}
Warning
With OnPush, mutating an object passed as an input will NOT trigger change detection. You must pass a new object reference. This is why immutable data patterns pair perfectly with OnPush.
OnPush + Immutable Data

TS
// Parent component
@Component({ selector: 'app-parent' })
export class ParentComponent {
  user = { name: 'Alice', email: 'alice@example.com' };

  updateName(newName: string): void {
    // ✗ WRONG — mutates the object; OnPush child won't re-render
    this.user.name = newName;

    // ✓ CORRECT — creates a new object reference
    this.user = { ...this.user, name: newName };
  }
}
OnPush with AsyncPipe

The async pipe integrates deeply with OnPush. It calls markForCheck() internally when the observable emits, so the component re-renders even under OnPush:

TS
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  standalone: true,
  imports: [AsyncPipe, CurrencyPipe],
  template: `
    @if (portfolio$ | async; as portfolio) {
      @for (stock of portfolio.stocks; track stock.symbol) {
        <div>{{ stock.symbol }}: {{ stock.price | currency }}</div>
      }
    }
  `,
})
export class PortfolioComponent {
  portfolio$ = this.marketService.getLivePortfolio();
  constructor(private marketService: MarketService) {}
}
Tip
The combination of OnPush + AsyncPipe is the performance sweet spot for data-driven Angular components. The component is dormant unless the observable emits.
Manual Change Detection with ChangeDetectorRef

Sometimes you need to trigger or suppress change detection manually. Inject ChangeDetectorRef:

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

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: 'app-data-grid',
})
export class DataGridComponent {
  private cdr = inject(ChangeDetectorRef);
  data: Record<string, unknown>[] = [];

  // Called from a third-party library callback (outside Angular zones)
  onDataUpdate(newData: Record<string, unknown>[]): void {
    this.data = newData;
    // Manually notify Angular that this component needs to be checked
    this.cdr.markForCheck();
  }

  // Immediately run CD for this component and its ancestors
  forceRefresh(): void {
    this.cdr.detectChanges();
  }

  // Detach the component — completely opt out of automatic CD
  pauseUpdates(): void {
    this.cdr.detach();
  }

  // Re-attach and resume
  resumeUpdates(): void {
    this.cdr.reattach();
    this.cdr.markForCheck();
  }
}

Method

Effect

markForCheck()

Marks this component and ancestors as dirty — CD will check them next cycle

detectChanges()

Runs CD synchronously for this component and its children now

detach()

Removes component from automatic CD — it will never check unless you call detectChanges()

reattach()

Re-attaches the component to the CD tree

checkNoChanges()

Asserts no changes — throws if bindings changed (development only)

Signals and Change Detection (Angular 17+)

Signals bypass the traditional Zone.js-based CD mechanism entirely. When a signal used in a template changes, Angular schedules a re-render for that specific component — no matter which CD strategy is used:

TS
import { Component, signal, computed, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app-metrics',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <p>Requests: {{ requestCount() }}</p>
    <p>Success rate: {{ successRate() }}%</p>
  `,
})
export class MetricsComponent {
  requestCount = signal(0);
  successCount = signal(0);

  successRate = computed(() => {
    const total = this.requestCount();
    const success = this.successCount();
    return total > 0 ? Math.round((success / total) * 100) : 0;
  });

  recordRequest(success: boolean) {
    this.requestCount.update(n => n + 1);
    if (success) this.successCount.update(n => n + 1);
    // Angular automatically schedules a re-render for this component
    // No Zone.js, no markForCheck() needed
  }
}
Note
This is Angular's direction: signals enable fine-grained reactivity that renders individual components without traversing the entire component tree.
NgZone and Running Outside Angular

For performance-critical code that should not trigger CD (animations, polling, background processing), run it outside Angular's zone:

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

@Component({ selector: 'app-animation' })
export class AnimationComponent {
  private ngZone = inject(NgZone);
  private cdr = inject(ChangeDetectorRef);

  startAnimation(): void {
    // Run the requestAnimationFrame loop outside Angular
    // No CD triggered on every frame
    this.ngZone.runOutsideAngular(() => {
      const loop = () => {
        this.updateCanvas(); // pure DOM/canvas manipulation

        if (this.isRunning) {
          requestAnimationFrame(loop);
        } else {
          // Re-enter Angular zone to update component state
          this.ngZone.run(() => {
            this.finalScore = this.calculateScore();
          });
        }
      };
      requestAnimationFrame(loop);
    });
  }

  isRunning = true;
  finalScore = 0;
  private updateCanvas() { /* canvas drawing */ }
  private calculateScore() { return 100; }
}
Zoneless Angular (Experimental in Angular 18+)

Angular 18 introduced experimental Zoneless mode. With Zoneless, Zone.js is completely removed and Angular relies entirely on signals (and explicit markForCheck() calls) for change detection:

TS
// main.ts — opt into Zoneless
import { bootstrapApplication } from '@angular/platform-browser';
import { provideExperimentalZonelessChangeDetection } from '@angular/core';

bootstrapApplication(AppComponent, {
  providers: [
    provideExperimentalZonelessChangeDetection(),
    // No more ZoneJS — smaller bundle, faster startup
  ],
});

JSON
// angular.json — remove zone.js from polyfills
{
  "polyfills": []
}
Warning
Zoneless mode is experimental in Angular 18/19. In Zoneless mode, components that rely on Zone.js triggers (like third-party libraries) may stop updating. Migrate state to signals or use markForCheck() where needed.
Performance Checklist
  • Use ChangeDetectionStrategy.OnPush on all presentational/leaf components

  • Pass immutable data to OnPush components — always create new object references

  • Use the async pipe to automatically handle Observable subscriptions and CD integration

  • Use signals for local state — they trigger targeted re-renders without Zone.js overhead

  • Run performance-critical loops (canvas, WebGL, polling) outside Angular with ngZone.runOutsideAngular()

  • Use trackBy in @for loops to prevent unnecessary DOM destruction/recreation

  • Avoid calling methods in templates — they run on every CD cycle; use computed signals or getters instead

  • Profile CD with Angular DevTools to identify slow components

Common CD Bugs and Fixes

Symptom

Likely Cause

Fix

Template not updating after data change

Object mutated instead of replaced

Create new reference: { ...old, key: value }

OnPush component stuck

Input mutated in parent

Use immutable updates in parent

Data updates from outside Angular

Third-party callback outside Zone.js

Call markForCheck() or use ngZone.run()

ExpressionChangedAfterItHasBeenChecked

Value changed during CD cycle

Use setTimeout(0) or restructure initialization

CD running too often

Using functions in templates

Replace with computed signals or memoized properties

Summary

Change detection is one of Angular's most important performance levers:

  • Default CD: checks all components on every async event — simple but can be slow for large trees
  • OnPush CD: only checks when inputs change, events fire, or async emits — much more efficient
  • Signals: provide fine-grained, targeted updates without Zone.js overhead — the future of Angular CD
  • ChangeDetectorRef methods give manual control when needed
  • Zoneless mode (Angular 18+) removes Zone.js entirely, relying on signals

Master these patterns and your Angular apps will be fast and responsive at any scale.