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.OnPushin all components - ✓Replace class properties with
signal() - ✓Use
computed()for derived state - ✓Use
input()andoutput()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!
}
}