Zoneless Change Detection (v18-21)

📚 What is Zoneless Angular?

Zoneless means running Angular without zone.js:

  • No zone.js: Removes the monkey-patching of async APIs
  • Smaller bundles: zone.js is ~15KB minified
  • Better performance: No unnecessary change detection cycles
  • Signal-driven: Change detection triggered by signals
  • Explicit triggers: Use markForCheck() or signals

📅 Zoneless Version History

v18Experimental zoneless (provideExperimentalZonelessChangeDetection)
v19Developer preview (provideZonelessChangeDetection)
v21Stable release ready for production

🎯 Interview Questions

  • Q1: What is zone.js and why remove it?
  • A: zone.js patches async APIs (setTimeout, Promise, etc.) to trigger change detection. Removing it reduces bundle size and gives more control.
  • Q2: How does change detection work without zone.js?
  • A: Angular schedules change detection when signals change, or when you explicitly call markForCheck(), ChangeDetectorRef.detectChanges(), or ApplicationRef.tick().
  • Q3: What changes are needed for zoneless?
  • A: Use signals for state, use OnPush change detection, avoid relying on automatic detection from async operations.
  • Q4: Can existing apps migrate to zoneless?
  • A: Yes, gradually. Start with OnPush and signals, then remove zone.js. Some third-party libs may need updates.

🔧 Zoneless-Ready Demo

Signal-based State (Zoneless Ready ✅)

0

✅ Uses signal - automatically triggers change detection

Computed Values (Zoneless Ready ✅)

Count × 2 = 0

Count × 10 = 0

Async Operations with Signals

✅ Signals update after async, triggering change detection

⚙️ How to Enable Zoneless

// app.config.ts
import { provideZonelessChangeDetection } from '@angular/core';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZonelessChangeDetection(),
    // ... other providers
  ]
};

// Also remove zone.js from angular.json polyfills
// "polyfills": [] // Remove "zone.js"

📋 Migration Checklist

  • Use ChangeDetectionStrategy.OnPush in all components
  • Replace class properties with signal()
  • Use computed() for derived state
  • Use input() and output() instead of decorators
  • Update signals in async callbacks (they'll trigger CD)
  • ⚠️Check third-party libraries for zone.js dependencies

💻 Zoneless Code Examples


// ===== Enabling Zoneless (v18+ experimental, v19+ preview, v21 stable) =====

// app.config.ts
import { provideZonelessChangeDetection } from '@angular/core';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZonelessChangeDetection(), // v19+
    // Or for v18:
    // provideExperimentalZonelessChangeDetection(),
  ]
};

// ===== Zoneless-Ready Component =====

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush, // Required!
  template: `
    <p>Count: {{ count() }}</p>
    <button (click)="increment()">+</button>
  `
})
export class CounterComponent {
  count = signal(0);
  
  increment() {
    this.count.update(c => c + 1);
    // Signal change automatically schedules change detection
  }
}

// ===== Handling Async Operations =====

@Component({...})
export class DataComponent {
  data = signal<Data | null>(null);
  loading = signal(false);
  
  async loadData() {
    this.loading.set(true);
    
    try {
      const result = await fetch('/api/data').then(r => r.json());
      this.data.set(result); // ✅ Triggers change detection
    } finally {
      this.loading.set(false);
    }
  }
}

// ===== Manual Change Detection (when needed) =====

@Component({...})
export class LegacyComponent {
  private cdr = inject(ChangeDetectorRef);
  
  // For non-signal state that changes outside Angular
  legacyData: string;
  
  externalCallback() {
    this.legacyData = 'updated';
    this.cdr.markForCheck(); // Manual trigger
  }
}

// ===== What NOT to do in Zoneless =====

// ❌ Mutating regular properties won't trigger updates
@Component({...})
export class BadComponent {
  count = 0; // ❌ Not a signal
  
  increment() {
    this.count++; // ❌ Won't update UI in zoneless!
  }
}

// ✅ Use signals instead
@Component({...})
export class GoodComponent {
  count = signal(0);
  
  increment() {
    this.count.update(c => c + 1); // ✅ Works!
  }
}