A zoneless application is an Angular app that operates without the zone.js library. This is a major architectural decision that provides more granular control over change detection and improves performance by eliminating the overhead of Zone.js’s automatic change detection triggers. This is introduced with the introduction to Angular Signals.
Architectural Implications:
- Explicit Change Detection: You are now 100% responsible for telling Angular when to check for changes. Automatic updates from setTimeout, events, or Promise resolutions will no longer happen.
- Primacy of Signals: Signals become the primary mechanism for state management. When a signal’s value changes, Angular knows precisely which components that consume that signal need to be re-rendered, without needing to check the entire component tree.
- Rethinking Asynchronous Code: Libraries or native browser APIs that rely on Zone.js to trigger UI updates will need to be handled manually. You’ll often use patterns like toSignal from @angular/core/rxjs-interop or manually call ChangeDetectorRef.detectChanges() in specific, controlled scenarios.
Practical Steps to Go Zoneless:
1. Bootstrap without zone.js
In your main.ts file, you configure the application bootstrap to be zoneless.
// in main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { provideRouter } from '@angular/router';
import { appRoutes } from './app/app.routes';
bootstrapApplication(AppComponent, { providers: [ provideRouter(appRoutes), { provide: 'ngZone', useValue: 'noop' } // The key step! ]
});
2. Convert State to Signals
All component states that can change and affect the view must be managed with signals (signal, computed, effect).
import { Component, signal, computed, effect } from '@angular/core';
@Component({ selector: 'app-counter', standalone: true, template: ` <p>Count: {{ count() }}</p><p>Double: {{ double() }}</p><button (click)="increment()">Increment</button> `
})
export class CounterComponent { count = signal(0); double = computed(() => this.count() * 2); constructor() { // Effects run automatically when their dependent signals change effect(() => { console.log(`The count is now: ${this.count()}`); }); } increment() { this.count.update(c => c + 1); // NO ChangeDetectorRef.detectChanges() needed! // The template is bound to the signal, and Angular handles the update. }
}
3. Handle External Asynchronicity
For things that aren’t signals (e.g., a third-party library’s event callback), you must manually integrate them.
import { Component, signal, ChangeDetectorRef, inject } from '@angular/core';
declare const thirdPartyLibrary: any; // Assume this library exists
@Component({...})
export class ThirdPartyComponent { data = signal<string>('Initial data'); private cdr = inject(ChangeDetectorRef); ngOnInit() { // This callback is outside Angular's world thirdPartyLibrary.onDataReceived((newData) => { this.data.set(newData); // In a zoneless app with a non-signal binding, you might need this. // However, the best practice is to have the template bind directly to the signal. // If the template was `<div>{{ rawDataProperty }}</div>`, you would need: // this.rawDataProperty = newData; // this.cdr.detectChanges(); }); }
}
Final Words
Zoneless Angular apps give you more control and better performance by removing the overhead of zone.js. But this also means you’re fully in charge of when and how UI updates happen. That’s where Angular Signals come in. They manage the state precisely, it adopts this setup smoothly. It also helps to hire Angular developers who understand signal-based reactivity and manual change detection.