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 and setInterval
  • Promise
  • XMLHttpRequest and fetch
  • 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:

  1. Button click ? Zone.js detects DOM event
  2. HTTP request ? Zone.js patches XMLHttpRequest
  3. Response arrives ? Zone.js triggers Change Detection
  4. 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.

By Shabazz

Software Engineer, MCSD, Web developer & Angular specialist , BizDevOps

Leave a Reply

Your email address will not be published. Required fields are marked *