With Angular 20.2, an important milestone has been reached: Zoneless Change Detection is now production-ready. But what does this mean for Angular developers? In this article, we’ll explain Zone.js from the ground up and show how the transition to zoneless architecture works.
What is Zone.js?
Zone.js is a JavaScript library that monitors and manages asynchronous operations in web applications. Think of Zone.js as a “detective” that observes all asynchronous activities in your application.
How Zone.js Works
Zone.js “patches” (overrides) native browser APIs such as:
setTimeout
andsetInterval
Promise
XMLHttpRequest
andfetch
- DOM Events
requestAnimationFrame

Zone.js VS Zoneless : Comparison

Detailed Code Examples
1. Traditional Angular Component with Zone.js
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-user-list',
template: `
<div>
<h2>Users ({{ users.length }})</h2>
<button (click)="loadUsers()">Load Users</button>
<ul>
<li *ngFor="let user of users">{{ user.name }}</li>
</ul>
</div>
`
})
export class UserListComponent {
users: any[] = [];
constructor(private http: HttpClient) {}
loadUsers() {
// Zone.js automatically detects the HTTP request
// and triggers Change Detection after the response
this.http.get<any[]>('/api/users').subscribe(users => {
this.users = users; // Change Detection runs automatically
});
}
}
What happens here with Zone.js:
- Button click ? Zone.js detects DOM event
- HTTP request ? Zone.js patches
XMLHttpRequest
- Response arrives ? Zone.js triggers Change Detection
users
array is updated ? DOM is re-rendered
2. Migration to OnPush (Preparation for Zoneless)
import { Component, ChangeDetectionStrategy, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-user-list',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div>
<h2>Users ({{ users().length }})</h2>
<button (click)="loadUsers()">Load Users</button>
<ul>
<li *ngFor="let user of users()">{{ user.name }}</li>
</ul>
<div *ngIf="loading()">Loading...</div>
</div>
`
})
export class UserListComponent {
private http = inject(HttpClient);
// Signals for reactive state management
users = signal<any[]>([]);
loading = signal(false);
loadUsers() {
this.loading.set(true);
this.http.get<any[]>('/api/users').subscribe(users => {
this.users.set(users); // Signal update automatically triggers Change Detection
this.loading.set(false);
});
}
}
3. Fully Zoneless with provideZonelessChangeDetection
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideZonelessChangeDetection } from '@angular/core';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [
provideZonelessChangeDetection(), // Enable zoneless
// other providers...
]
});
// user-list.component.ts
import { Component, ChangeDetectionStrategy, inject, signal, effect } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-user-list',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div>
<h2>Users ({{ users().length }})</h2>
<button (click)="loadUsers()">Load Users</button>
<!-- Async pipe also works zoneless -->
<div *ngIf="users$ | async as userList">
<ul>
<li *ngFor="let user of userList">{{ user.name }}</li>
</ul>
</div>
<div *ngIf="loading()">Loading...</div>
</div>
`
})
export class UserListComponent {
private http = inject(HttpClient);
users = signal<any[]>([]);
loading = signal(false);
users$ = this.http.get<any[]>('/api/users'); // Observable for async pipe
constructor() {
// Effect for side effects
effect(() => {
console.log(`Number of users: ${this.users().length}`);
});
}
loadUsers() {
this.loading.set(true);
this.http.get<any[]>('/api/users').subscribe({
next: users => {
this.users.set(users);
this.loading.set(false);
},
error: () => {
this.loading.set(false);
}
});
}
}
Migration Step-by-Step
Step 1: Convert Components to OnPush
// Before: Default Change Detection
@Component({
selector: 'app-example',
template: `<div>{{ data }}</div>`
})
export class ExampleComponent {
data = 'initial';
updateData() {
setTimeout(() => {
this.data = 'updated'; // Works with Zone.js
}, 1000);
}
}
// After: OnPush with Signals
@Component({
selector: 'app-example',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<div>{{ data() }}</div>`
})
export class ExampleComponent {
data = signal('initial');
updateData() {
setTimeout(() => {
this.data.set('updated'); // Signal triggers Change Detection
}, 1000);
}
}
Step 2: Remove Zone.js
// angular.json - Remove Zone.js
{
"projects": {
"my-app": {
"architect": {
"build": {
"options": {
"polyfills": [
// "zone.js" <- Remove this line
]
}
}
}
}
}
}
Step 3: Enable Zoneless Change Detection
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideZonelessChangeDetection } from '@angular/core';
bootstrapApplication(AppComponent, {
providers: [
provideZonelessChangeDetection(),
// other providers
]
});
Common Pitfalls and Solutions
Problem: Third-party Libraries
// Problematic: Library uses setTimeout without Signal update
someLibrary.doAsyncWork().then(result => {
this.result = result; // No Change Detection!
});
// Solution: Manually trigger Change Detection
import { ChangeDetectorRef, inject } from '@angular/core';
export class MyComponent {
private cdr = inject(ChangeDetectorRef);
async doWork() {
const result = await someLibrary.doAsyncWork();
this.result = result;
this.cdr.markForCheck(); // Manually trigger Change Detection
}
}
// Better solution: Use Signals
export class MyComponent {
result = signal(null);
async doWork() {
const result = await someLibrary.doAsyncWork();
this.result.set(result); // Automatic Change Detection
}
}
When Should we Switch to Zoneless?

- New projects with Angular 18+
- Performance-critical applications
- Mobile apps (smaller bundle size)
- Projects with modern Angular patterns (Signals, OnPush)

- Legacy applications with many Default Change Detection components
- Projects with many third-party libraries of unknown compatibility
- Teams without experience with OnPush and Signals
New Angular CLI Option
With Angular 20.2, you can create a zoneless project directly:
# New zoneless project
ng new my-zoneless-app --zoneless
# Or for existing projects
ng add @angular/core --zoneless
Advanced Zoneless Patterns
Using Signals with Computed Values
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-shopping-cart',
template: `
<div>
<h2>Shopping Cart</h2>
<p>Items: {{ itemCount() }}</p>
<p>Total: ${{ totalPrice() }}</p>
<button (click)="addItem()">Add Item</button>
</div>
`
})
export class ShoppingCartComponent {
private items = signal([
{ name: 'Apple', price: 1.50 },
{ name: 'Banana', price: 0.75 }
]);
// Computed signals automatically update when dependencies change
itemCount = computed(() => this.items().length);
totalPrice = computed(() =>
this.items().reduce((sum, item) => sum + item.price, 0)
);
addItem() {
this.items.update(items => [
...items,
{ name: 'Orange', price: 2.00 }
]);
// Both itemCount() and totalPrice() will automatically update
}
}
Migration Checklist
Pre-Migration Assessment
- Audit Components: Identify components using Default Change Detection
- Library Compatibility: Check if third-party libraries support zoneless
- Test Coverage: Ensure comprehensive tests before migration
- Team Training: Educate team on Signals and OnPush patterns
Migration Steps
- Step 1: Convert components to OnPush strategy
- Step 2: Replace direct property assignments with Signals
- Step 3: Update event handlers to use Signal updates
- Step 4: Test thoroughly with Zone.js still enabled
- Step 5: Remove Zone.js from polyfills
- Step 6: Enable
provideZonelessChangeDetection()
- Step 7: Final testing and performance validation
Conclusion
Zone.js has been a crucial building block for Angular’s success by automating Change Detection. With the introduction of Signals and Zoneless Change Detection in Angular 20.2, developers now have a choice between:
- Simplicity (Zone.js) – Automatic, but with performance overhead
- Performance (Zoneless) – Optimized, but requires more conscious programming
The future of Angular is clearly moving towards Zoneless, but the transition is gradual and developer-friendly. Zone.js will continue to be supported, so existing applications don’t need to migrate immediately.
Recommendation: Start new projects with Zoneless and migrate existing applications gradually when the benefits justify the effort. The improved performance, smaller bundle size, and cleaner debugging experience make Zoneless an attractive option for modern Angular applications.
The transition to Zoneless represents Angular’s evolution towards more explicit, performant, and maintainable code patterns. While it requires developers to be more intentional about state management, the benefits in terms of performance and debugging make it a worthwhile investment for the future.