Zoneless Angular - Production Ready (v21)
📚 Zoneless is Stable!
Angular 21 marks zoneless mode as production-ready:
- No Zone.js: Remove 12KB+ from bundle
- Better performance: No unnecessary change detection
- Signal-driven: Components update via signals
- Simpler debugging: Stack traces without Zone patches
- Framework integration: Works with all Angular features
🛤️ Migration Path
1
Enable OnPush everywhere
All components use ChangeDetectionStrategy.OnPush
2
Convert to Signals
Replace mutable properties with signal(), input(), model()
3
Remove async triggers
Replace setTimeout-based updates with signal updates
4
Enable zoneless
Add provideZonelessChangeDetection() to config
🎯 Interview Questions
- Q1: What triggers change detection in zoneless mode?
- A: Signal changes, event handlers (still work!), async pipe, markForCheck(). NOT setTimeout/setInterval or plain property assignments.
- Q2: Do event handlers still work without Zone.js?
- A: Yes! Angular's event handlers (() = method()) automatically trigger change detection.
- Q3: What about third-party libraries?
- A: Libraries that rely on Zone.js patching need updates. Signal-based libraries work fine.
- Q4: How much smaller is the bundle?
- A: Zone.js is ~12-15KB gzipped. Removing it also improves startup time.
🔧 Zoneless-Ready Demo
Signal-Based Counter (Zoneless Ready)
0
✅ Event handlers work in zoneless
✅ Signal updates trigger change detection
✅ OnPush strategy enabled
What Would Break Without Signals?
// ❌ This won't update UI in zoneless:
setTimeout(() => {
this.value = 'new'; // Plain property!
}, 1000);
// ✅ This works in zoneless:
setTimeout(() => {
this.value.set('new'); // Signal!
}, 1000);
✅ Zoneless Readiness Checklist
Progress: 0/6 items
💻 Zoneless Setup Code
// ===== Enable Zoneless in Angular 21+ =====
// app.config.ts
import {
ApplicationConfig,
provideZonelessChangeDetection
} from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideZonelessChangeDetection(), // That's it!
provideRouter(routes),
provideHttpClient(),
]
};
// ===== Remove zone.js from polyfills =====
// angular.json - remove from polyfills array
{
"build": {
"options": {
"polyfills": [
// "zone.js" <-- Remove this!
]
}
}
}
// Or in package.json, remove zone.js dependency entirely
// ===== Component Patterns for Zoneless =====
@Component({
changeDetection: ChangeDetectionStrategy.OnPush, // Required!
template: `
<button (click)="increment()">Count: {{ count() }}</button>
`
})
export class ZonelessComponent {
count = signal(0);
// ✅ Event handlers work - Angular handles them
increment() {
this.count.update(v => v + 1);
}
// ✅ Signal updates trigger change detection
loadData() {
fetch('/api/data')
.then(res => res.json())
.then(data => this.data.set(data)); // Signal update!
}
// ✅ Using resource() - recommended for HTTP
data = resource({
loader: async () => {
const res = await fetch('/api/data');
return res.json();
}
});
}
// ===== What DOESN'T Work Without Signals =====
@Component({...})
export class BrokenZonelessComponent {
value = 'initial'; // ❌ Plain property
updateLater() {
// ❌ Won't trigger UI update in zoneless!
setTimeout(() => {
this.value = 'updated';
}, 1000);
}
// ❌ Interval won't update UI
ngOnInit() {
setInterval(() => {
this.value = 'ticking'; // Nothing happens!
}, 1000);
}
}
// ===== Fixed Version =====
@Component({...})
export class FixedZonelessComponent {
value = signal('initial'); // ✅ Signal
updateLater() {
// ✅ Works! Signal update triggers CD
setTimeout(() => {
this.value.set('updated');
}, 1000);
}
// ✅ Effect with cleanup for intervals
constructor() {
effect((onCleanup) => {
const interval = setInterval(() => {
this.value.set('ticking'); // Works!
}, 1000);
onCleanup(() => clearInterval(interval));
});
}
}
// ===== Testing Without Zone =====
// Use async/await instead of fakeAsync
describe('ZonelessComponent', () => {
it('should update count', async () => {
const fixture = TestBed.createComponent(ZonelessComponent);
const component = fixture.componentInstance;
component.increment();
fixture.detectChanges(); // Manual CD trigger
expect(component.count()).toBe(1);
});
});
// ===== Manual Change Detection (if needed) =====
@Component({...})
export class ManualCDComponent {
private cdr = inject(ChangeDetectorRef);
// For rare cases where you need manual trigger
forceUpdate() {
this.cdr.markForCheck();
// or
this.cdr.detectChanges();
}
}
// ===== Framework Features That Work in Zoneless =====
// ✅ Event bindings ((click), (input), etc.)
// ✅ Signal updates
// ✅ Async pipe (internally uses markForCheck)
// ✅ Router navigation
// ✅ Form controls (reactive & template-driven)
// ✅ Animations
// ✅ HTTP interceptors
// ✅ resource() / httpResource()