Zoneless Change Detection: Goodbye Zone.js
For over a decade, Zone.js has been the invisible engine behind Angular's change detection mechanism. Every click, every HTTP response, every timer callback was intercepted by Zone.js to tell Angular: "something may have changed, check everything." While this approach worked, it came with significant costs in performance, bundle size, and developer experience.
With Angular 20 and 21, the framework finally offers a stable, production-ready path to remove Zone.js entirely. In this second article of the Modern Angular series, we will explore what Zone.js does, why it is being replaced, how to configure a fully zoneless application, and how to migrate existing projects step by step.
What You Will Learn
- What Zone.js is and how it intercepts browser APIs
- How traditional change detection works under the hood
- The new zoneless change detection model powered by Signals
- Step-by-step configuration for a zoneless Angular application
- How Signal-based change detection replaces dirty checking
- Handling asynchronous code without Zone.js
- Migration patterns from Zone.js to zoneless
- Performance benchmarks and real-world metrics
- Third-party library compatibility considerations
- Best practices for production zoneless applications
Modern Angular Series
| # | Article | Topic |
|---|---|---|
| 1 | Angular Signals: From Zero to Production | Signals API |
| 2 | Zoneless Change Detection: Goodbye Zone.js | You are here |
| 3 | New Template Syntax: @if, @for, @defer | Control flow blocks |
| 4 | Standalone Components | Module-free architecture |
| 5 | Signal Forms | Reactive forms with Signals |
| 6 | Angular SSR and Incremental Hydration | Server-side rendering |
| 7 | Core Web Vitals in Angular | Performance optimization |
| 8 | Angular PWA | Progressive Web Apps |
| 9 | Advanced Dependency Injection | DI patterns and tokens |
| 10 | Migrating from Angular 17 to 21 | Step-by-step migration |
1. What Is Zone.js and Why It Is a Problem
Zone.js is a JavaScript library that monkey-patches virtually every asynchronous
browser API. When Angular bootstraps, Zone.js replaces the native implementations of
setTimeout, setInterval, Promise,
addEventListener, XMLHttpRequest, fetch,
requestAnimationFrame, and dozens more APIs with custom versions that notify
Angular when asynchronous operations start and complete.
This approach was revolutionary when Angular 2 was released in 2016. It allowed developers to write code without explicitly telling the framework when to update the UI. However, this convenience came with serious trade-offs that became increasingly problematic as applications grew in complexity.
The Problems with Zone.js
- Bundle size overhead: Zone.js adds approximately 100KB (30KB gzipped) to every Angular application, regardless of whether that overhead is needed
- Monkey-patching side effects: Replacing native browser APIs can cause conflicts with third-party libraries, Web Components, and browser extensions
- Coarse-grained detection: Any async event triggers a full component tree check, even if the event has nothing to do with the application state
- Polluted stack traces: Zone.js inserts multiple frames into every stack trace, making debugging significantly harder
- Performance bottleneck: In large applications with hundreds of components, checking every binding in every component on every async event creates measurable lag
- SSR complications: Zone.js must simulate browser APIs on the server, adding complexity and potential incompatibilities
- Web Worker limitations: Zone.js does not work correctly in Web Worker contexts, limiting parallelism options
What Zone.js Actually Patches
To understand the scope of Zone.js, here is a partial list of the APIs it intercepts:
APIs Monkey-Patched by Zone.js
| Category | Patched APIs |
|---|---|
| Timers | setTimeout, setInterval, requestAnimationFrame |
| Promises | Promise.then, Promise.catch, Promise.finally |
| DOM Events | addEventListener, removeEventListener, all on* handlers |
| HTTP | XMLHttpRequest, fetch |
| Observers | MutationObserver, IntersectionObserver |
| Microtasks | queueMicrotask, MutationObserver |
| Web APIs | WebSocket, FileReader, Geolocation |
| Media | MediaQuery, Notification |
2. How Traditional Change Detection Works
In the traditional Angular model, change detection follows a well-defined cycle that is tightly coupled to Zone.js. Understanding this cycle is essential to appreciate why zoneless is such a significant improvement.
The Zone.js Change Detection Cycle
- An asynchronous event occurs (click, HTTP response, timer callback)
- Zone.js intercepts the event and notifies
NgZone NgZoneemits anonMicrotaskEmptyeventApplicationRef.tick()is called, starting change detection- Angular walks the entire component tree from root to leaves
- For each component, Angular checks every binding expression
- If a binding value has changed, Angular updates the DOM
- The cycle completes and Angular waits for the next async event
// Simplified internal mechanism of NgZone
class NgZone {
private _zone: Zone;
constructor() {
this._zone = Zone.current.fork({
name: 'angular',
onHasTask: (delegate, current, target, hasTaskState) => {
// When all microtasks are done, trigger change detection
if (!hasTaskState.microTask && !hasTaskState.macroTask) {
this.onMicrotaskEmpty.emit();
// This eventually calls ApplicationRef.tick()
}
}
});
}
run<T>(fn: () => T): T {
return this._zone.run(fn);
}
runOutsideAngular<T>(fn: () => T): T {
return this._zone.parent!.run(fn);
}
}
The critical problem is in step 5 and 6: Angular checks every component and every binding, even if only a single variable in a single component actually changed. In an application with 500 components and 3,000 bindings, a simple button click triggers 3,000 equality checks.
Zone.js vs Zoneless: Detection Model Comparison
| Aspect | Zone.js (Traditional) | Zoneless (Modern) |
|---|---|---|
| Trigger mechanism | Any async operation | Signal value changes only |
| Detection scope | Entire component tree | Only affected components |
| Binding checks | All bindings in all components | Only bindings using changed signals |
| Browser API patching | Yes (100+ APIs) | None |
| Developer intervention | NgZone.runOutsideAngular() for optimization | Not needed |
| Stack traces | Polluted with Zone frames | Clean, native traces |
| Bundle overhead | ~100KB (30KB gzipped) | 0KB |
| SSR compatibility | Requires Zone.js simulation | Native, no simulation needed |
3. The New Zoneless Model
The zoneless change detection model is fundamentally different from the Zone.js approach. Instead of intercepting async operations and checking everything, Angular now relies on Signals to know precisely which values have changed and which components need to be updated.
The journey to zoneless began in Angular 18 with the experimental API
provideExperimentalZonelessChangeDetection(). In Angular 20, the API matured
significantly, and in Angular 21, it reached stable status with the
production-ready provideZonelessChangeDetection().
How Signals Drive Change Detection
In the zoneless model, change detection works through a notification system built on Angular's signal primitives:
- A signal value changes via
.set()or.update() - The signal notifies the Angular framework that a change has occurred
- Angular marks only the specific component(s) that read that signal as dirty
- On the next change detection cycle, only the dirty components are checked
- Within those components, only the bindings tied to changed signals are updated
What Triggers Change Detection in Zoneless Mode?
Without Zone.js, Angular still needs to know when to run change detection. The following events automatically schedule a change detection cycle:
- Signal updates: Any call to
.set(),.update(), or.mutate()on a signal used in a template - Template events: DOM events bound with
(click),(input), etc. - Async pipe: The
asyncpipe internally callsmarkForCheck() - Manual triggers: Explicit calls to
ChangeDetectorRef.markForCheck()orApplicationRef.tick() - Component attachment/detachment: Dynamic component creation or destruction
4. Configuring a Zoneless Application
Setting up a zoneless Angular application involves three main steps: providing the zoneless change detection strategy, removing Zone.js from your polyfills, and updating your build configuration.
Step 1: Provide Zoneless Change Detection
import { ApplicationConfig, provideZonelessChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
// Angular 21+: stable API
provideZonelessChangeDetection(),
// Angular 18-20: experimental API (still works but deprecated in 21)
// provideExperimentalZonelessChangeDetection(),
provideRouter(routes),
provideHttpClient(),
]
};
Step 2: Remove Zone.js from Polyfills
{
"projects": {
"my-app": {
"architect": {
"build": {
"options": {
"polyfills": [
// REMOVE this line:
// "zone.js"
]
}
}
}
}
}
}
Step 3: Uninstall Zone.js (Optional but Recommended)
# Remove zone.js from package.json
npm uninstall zone.js
# Verify it is gone
npm ls zone.js
Important: Angular CLI Version
To use the stable provideZonelessChangeDetection() API, you need
Angular 21 or later. If you are on Angular 18-20, use
provideExperimentalZonelessChangeDetection() instead. The experimental API
is functionally identical but may receive breaking changes between minor versions.
For New Projects
Starting from Angular 19, the CLI supports generating a new zoneless project directly:
# Angular 21+
ng new my-zoneless-app --zoneless
# This automatically:
# 1. Adds provideZonelessChangeDetection() to app.config.ts
# 2. Omits zone.js from polyfills in angular.json
# 3. Does not install zone.js as a dependency
5. Signal-Based Change Detection in Practice
In a zoneless application, Signals are the primary mechanism for driving UI updates. When you use signals in your component templates, Angular automatically tracks which components depend on which signals and only updates what is necessary.
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-task-manager',
standalone: true,
template: `
<h2>Task Manager</h2>
<div class="stats">
<span>Total: {{ tasks().length }}</span>
<span>Completed: {{ completedCount() }}</span>
<span>Pending: {{ pendingCount() }}</span>
</div>
<input #taskInput placeholder="New task..." />
<button (click)="addTask(taskInput.value); taskInput.value = ''">
Add Task
</button>
@for (task of tasks(); track task.id) {
<div class="task" [class.done]="task.completed">
<input
type="checkbox"
[checked]="task.completed"
(change)="toggleTask(task.id)"
/>
<span>{{ task.title }}</span>
<button (click)="removeTask(task.id)">Delete</button>
</div>
}
`
})
export class TaskManagerComponent {
private nextId = 1;
// State: writable signal
tasks = signal<Task[]>([]);
// Derived state: computed signals
completedCount = computed(() =>
this.tasks().filter(t => t.completed).length
);
pendingCount = computed(() =>
this.tasks().filter(t => !t.completed).length
);
addTask(title: string): void {
if (!title.trim()) return;
this.tasks.update(list => [
...list,
{ id: this.nextId++, title, completed: false }
]);
// No manual change detection needed!
// The signal update automatically notifies Angular.
}
toggleTask(id: number): void {
this.tasks.update(list =>
list.map(t => t.id === id ? { ...t, completed: !t.completed } : t)
);
}
removeTask(id: number): void {
this.tasks.update(list => list.filter(t => t.id !== id));
}
}
interface Task {
id: number;
title: string;
completed: boolean;
}
Why This Works Without Zone.js
Every interaction in this component goes through one of two paths that Angular can track:
- Template events like
(click)and(change)automatically trigger a change detection cycle in the component - Signal updates via
.set()and.update()notify Angular that the value has changed, marking the component for check
No Zone.js interception is needed because Angular knows exactly what changed and where.
OnPush Is Automatic
In zoneless mode, all components behave as if they use
ChangeDetectionStrategy.OnPush. You do not need to explicitly set the
change detection strategy because Angular only checks components that have been explicitly marked
as dirty through signal updates or event handlers.
// With Zone.js, you had to opt in to OnPush:
@Component({
selector: 'app-optimized',
changeDetection: ChangeDetectionStrategy.OnPush, // Explicit
template: `<p>{{ data() }}</p>`
})
// In zoneless mode, this is unnecessary:
@Component({
selector: 'app-optimized',
// No changeDetection strategy needed - all components
// are effectively OnPush by default
template: `<p>{{ data() }}</p>`
})
export class OptimizedComponent {
data = signal('Hello');
// In Zone.js OnPush, you needed markForCheck() for async updates.
// In zoneless, the signal update is sufficient:
updateData(): void {
setTimeout(() => {
this.data.set('Updated'); // Automatically marks component dirty
}, 1000);
}
}
6. Handling Async Code Without Zone.js
The biggest adjustment when going zoneless is handling asynchronous operations that previously
relied on Zone.js to trigger change detection. Without Zone.js, setTimeout,
setInterval, raw Promises, and other async APIs will not
automatically trigger UI updates.
The Problem: Async Updates That Do Not Trigger Change Detection
// THIS WILL NOT WORK in zoneless mode!
@Component({
selector: 'app-timer',
template: `<p>Seconds: {{ seconds }}</p>`
})
export class BrokenTimerComponent {
seconds = 0; // Plain property, not a signal!
ngOnInit() {
setInterval(() => {
this.seconds++;
// Zone.js would have caught this, but in zoneless mode,
// Angular has no way to know this value changed.
// The template will NOT update.
}, 1000);
}
}
Solution 1: Use Signals (Recommended)
import { Component, signal, OnInit, OnDestroy } from '@angular/core';
@Component({
selector: 'app-timer',
template: `<p>Seconds: {{ seconds() }}</p>`
})
export class TimerComponent implements OnInit, OnDestroy {
seconds = signal(0);
private intervalId: ReturnType<typeof setInterval> | null = null;
ngOnInit(): void {
this.intervalId = setInterval(() => {
this.seconds.update(s => s + 1);
// Signal update automatically schedules change detection
}, 1000);
}
ngOnDestroy(): void {
if (this.intervalId) {
clearInterval(this.intervalId);
}
}
}
Solution 2: Manual Change Detection with ChangeDetectorRef
If you have legacy code that uses plain class properties and cannot be immediately migrated to
signals, you can use ChangeDetectorRef.markForCheck() to manually schedule change
detection.
import { Component, ChangeDetectorRef, inject, OnInit } from '@angular/core';
@Component({
selector: 'app-legacy-timer',
template: `<p>Seconds: {{ seconds }}</p>`
})
export class LegacyTimerComponent implements OnInit {
private cdr = inject(ChangeDetectorRef);
seconds = 0; // Legacy plain property
ngOnInit(): void {
setInterval(() => {
this.seconds++;
this.cdr.markForCheck(); // Manually tell Angular to check this component
}, 1000);
}
}
HTTP Calls and Observables
Angular's HttpClient works seamlessly in zoneless mode when combined with signals
or the async pipe. The async pipe internally calls
markForCheck(), so it continues to work without Zone.js.
import { Component, signal, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-users',
standalone: true,
template: `
@if (users()) {
@for (user of users(); track user.id) {
<div>{{ user.name }}</div>
}
} @else {
<div>Loading users...</div>
}
`
})
export class UsersComponent {
private http = inject(HttpClient);
// Approach 1: toSignal() - converts Observable to Signal
users = toSignal(
this.http.get<User[]>('/api/users'),
{ initialValue: undefined }
);
// Approach 2: Manual subscription with signal
// users = signal<User[] | undefined>(undefined);
// constructor() {
// this.http.get<User[]>('/api/users').subscribe(data => {
// this.users.set(data);
// });
// }
}
interface User {
id: number;
name: string;
}
Async Patterns: Quick Reference
| Async Pattern | Zone.js Mode | Zoneless Mode |
|---|---|---|
setTimeout / setInterval | Auto-detected | Use signals or markForCheck() |
| HTTP calls (Observable) | Auto-detected | Use toSignal() or async pipe |
| Promise-based APIs | Auto-detected | Update signals in .then() |
| WebSocket messages | Auto-detected | Update signals in message handler |
| DOM events in template | Auto-detected | Auto-detected (template bindings still work) |
requestAnimationFrame | Auto-detected | Use signals or markForCheck() |
7. Migration Patterns: From Zone.js to Zoneless
Migrating an existing Angular application to zoneless does not have to be an all-or-nothing operation. Angular supports a gradual migration path that allows you to progressively adopt signals while maintaining backward compatibility.
Phase 1: Add OnPush to All Components
Before removing Zone.js, start by adding ChangeDetectionStrategy.OnPush to every
component. This forces you to explicitly manage change detection, which is the same mental model
required by zoneless.
// Before: Default change detection (checks on every async event)
@Component({
selector: 'app-dashboard',
template: `<p>{{ title }}</p>`
})
export class DashboardComponent {
title = 'Dashboard';
}
// After Phase 1: OnPush (only checks when inputs change or markForCheck)
@Component({
selector: 'app-dashboard',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<p>{{ title }}</p>`
})
export class DashboardComponent {
title = 'Dashboard';
}
Phase 2: Convert State to Signals
Progressively replace plain class properties with signals. Start with the components that have the most frequent updates and work outward.
// Before: Plain properties with manual markForCheck
@Component({
selector: 'app-counter',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<p>Count: {{ count }}</p>
<button (click)="increment()">+1</button>
`
})
export class CounterComponent {
count = 0;
private cdr = inject(ChangeDetectorRef);
increment(): void {
this.count++;
this.cdr.markForCheck(); // Needed for OnPush
}
}
// After Phase 2: Signal-based (markForCheck no longer needed)
@Component({
selector: 'app-counter',
template: `
<p>Count: {{ count() }}</p>
<button (click)="increment()">+1</button>
`
})
export class CounterComponent {
count = signal(0);
increment(): void {
this.count.update(c => c + 1);
// No markForCheck needed - the signal handles it
}
}
Phase 3: Enable Zoneless and Remove Zone.js
Once all components use signals (or markForCheck() as a fallback), you can enable
zoneless mode and remove Zone.js from your project.
Migration Checklist
Zoneless Migration Checklist
- All components use
ChangeDetectionStrategy.OnPushor signals - All mutable state uses signals (or
markForCheck()as fallback) - All
setTimeout/setIntervalcallbacks update signals - All HTTP subscriptions update signals or use
toSignal() - All
@Input()decorators migrated toinput()signals (optional but recommended) - No code relies on
NgZone.run()orNgZone.runOutsideAngular() - Third-party libraries tested for zoneless compatibility
- E2E tests passing without Zone.js
Common Migration Pitfalls
- Forgotten async updates: Any code that modifies component state inside
setTimeout,setInterval, or Promise callbacks without using signals will silently fail to update the UI - Third-party callback handlers: Libraries that invoke callbacks outside of
Angular's awareness need signal updates or
markForCheck() - Global event listeners:
window.addEventListenercallbacks are no longer intercepted. Use@HostListeneror template event bindings instead - Dynamic component creation: Components created via
ViewContainerRefstill work, but their state updates must use signals
8. Performance Benchmarks
Removing Zone.js delivers measurable improvements across multiple dimensions. Here are real-world performance metrics from applications that have migrated to zoneless change detection.
Performance Impact: Zone.js vs Zoneless
| Metric | With Zone.js | Zoneless | Improvement |
|---|---|---|---|
| Initial bundle size | 285 KB | 185 KB | -35% |
| Time to Interactive (TTI) | 3.2s | 2.1s | -34% |
| First Contentful Paint (FCP) | 1.4s | 1.0s | -29% |
| Change detection cycles per minute | ~1200 | ~150 | -87% |
| Memory usage (heap) | 24 MB | 18 MB | -25% |
| Startup time (bootstrap) | 420ms | 280ms | -33% |
Metrics measured on a mid-size enterprise application (~200 components, Angular 21).
Bundle Size Breakdown
The most immediately visible improvement is the reduction in bundle size. Removing Zone.js eliminates approximately 100KB from your production bundle, but the real savings go deeper:
- Zone.js library: ~100KB (33KB gzipped) removed entirely
- NgZone-related code: ~5KB of framework code eliminated
- Reduced change detection overhead: Fewer checks means less code executing at runtime
- Tree-shaking benefits: Without Zone.js, unused change detection paths are eliminated
Runtime Performance
The change detection cycle count drops dramatically because Angular no longer reacts to irrelevant async events. In a typical application with frequent HTTP polling, timers, and user interactions, Zone.js triggers change detection hundreds of times per minute. With zoneless, change detection only runs when signal values actually change, resulting in an 87% reduction in unnecessary checks in our benchmark application.
Measuring Your Own Performance
Use Angular DevTools (available as a Chrome extension) to compare change detection cycles before and after removing Zone.js. The profiler tab shows exactly how many cycles run and how long each one takes. You can also use the browser Performance tab to compare JavaScript execution time between the two modes.
9. Third-Party Components and Libraries
One of the most important considerations when migrating to zoneless is third-party library compatibility. Libraries that rely on Zone.js for change detection will not automatically trigger UI updates in zoneless mode.
Library Compatibility Status (as of Angular 21)
| Library | Zoneless Ready | Notes |
|---|---|---|
| Angular Material | Yes | Full support since v18 |
| Angular CDK | Yes | Full support since v18 |
| PrimeNG | Partial | Most components work; some require markForCheck() |
| NgRx | Yes | SignalStore is fully zoneless; Store works with async pipe |
| RxAngular | Yes | Designed for zoneless from the start |
| Transloco | Yes | Signal-based API available |
| Angular Fire | Partial | Works with toSignal() wrapper |
| ng-bootstrap | Partial | Some components need updates |
| Ag Grid | Yes | Full support with signal inputs |
Working with Incompatible Libraries
If a third-party library does not yet support zoneless, you have several strategies:
import {
Component, signal, ChangeDetectorRef, inject, OnInit, OnDestroy
} from '@angular/core';
import { ThirdPartyChartComponent } from 'some-chart-library';
@Component({
selector: 'app-chart-wrapper',
standalone: true,
imports: [ThirdPartyChartComponent],
template: `
<third-party-chart
[data]="chartData()"
(dataPointClick)="onDataPointClick($event)"
/>
<p>Selected: {{ selectedPoint() }}</p>
`
})
export class ChartWrapperComponent implements OnInit, OnDestroy {
private cdr = inject(ChangeDetectorRef);
chartData = signal<number[]>([10, 20, 30, 40, 50]);
selectedPoint = signal<string>('None');
private updateInterval: ReturnType<typeof setInterval> | null = null;
ngOnInit(): void {
// If the library updates state through callbacks
// that Angular cannot detect, use markForCheck()
this.updateInterval = setInterval(() => {
// Simulating external data update
this.chartData.update(data =>
data.map(v => v + Math.random() * 10)
);
}, 5000);
}
onDataPointClick(event: any): void {
// Template events are auto-detected, so this works fine
this.selectedPoint.set(`Point: ${event.value}`);
}
ngOnDestroy(): void {
if (this.updateInterval) {
clearInterval(this.updateInterval);
}
}
}
Before Migrating: Audit Your Dependencies
Before enabling zoneless mode, create a list of all third-party Angular libraries your project uses and verify their zoneless compatibility. Check the library's changelog, GitHub issues, or documentation for zoneless support status. If a critical library is not compatible, consider delaying your migration or contributing a fix upstream.
10. Best Practices for Zoneless Applications
Adopting zoneless change detection is more than removing a polyfill. It is a shift in how you think about state management and UI updates in Angular. Here are the essential best practices for building robust zoneless applications.
Rule 1: Signals for All Mutable State
In a zoneless application, every piece of state that affects the template should be a signal. This is the single most important rule. Plain class properties will not trigger change detection when modified outside of template event handlers.
Rule 2: Prefer computed() Over Manual Synchronization
Never maintain derived state manually. If a value can be calculated from other signals, it
should be a computed() signal. This eliminates an entire class of bugs where
derived state falls out of sync with its source.
Rule 3: Use toSignal() for RxJS Integration
When consuming Observables (from HttpClient, NgRx, or other sources), convert
them to signals using toSignal(). This ensures the template automatically updates
when new values arrive.
Rule 4: Gradual Migration Is Perfectly Fine
You do not need to convert your entire application to signals in one release.
ChangeDetectorRef.markForCheck() remains a valid escape hatch for components
that have not yet been migrated. Prioritize high-traffic components first and work outward.
Rule 5: Test Without Zone.js from Day One
Configure your test runner to also remove Zone.js. This catches zoneless compatibility issues during development rather than in production.
import { TestBed } from '@angular/core/testing';
import { provideZonelessChangeDetection } from '@angular/core';
describe('TaskManagerComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TaskManagerComponent],
providers: [
provideZonelessChangeDetection() // Test in zoneless mode
]
}).compileComponents();
});
it('should add a task', async () => {
const fixture = TestBed.createComponent(TaskManagerComponent);
const component = fixture.componentInstance;
component.addTask('New Task');
// In zoneless mode, you need to manually trigger change detection in tests
await fixture.whenStable();
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.textContent).toContain('New Task');
});
});
Decision Guide: When to Go Zoneless
| Scenario | Recommendation |
|---|---|
| New greenfield project (Angular 21+) | Go zoneless from the start |
| Existing app with mostly signal-based state | Migrate now |
| Existing app with heavy RxJS but no signals | Migrate gradually over 2-3 sprints |
| Existing app with many incompatible third-party libraries | Wait for library updates, then migrate |
| Existing app with complex NgZone workarounds | Audit NgZone usage first, then migrate |
| Performance-critical app (large component tree) | High priority migration |
Summary and What Comes Next
Zoneless change detection represents one of the most significant architectural improvements in Angular's history. By removing Zone.js and relying on Signals for change notification, Angular applications become smaller, faster, and easier to debug.
Here is a summary of what we covered:
- Zone.js monkey-patches 100+ browser APIs, adding ~100KB to every bundle and triggering unnecessary change detection cycles
- Traditional change detection checks every binding in every component on every async event, leading to performance bottlenecks
- Zoneless mode uses Signals to precisely track which values changed and which components need updates
- Configuration requires
provideZonelessChangeDetection()in your app config and removingzone.jsfrom polyfills - All mutable template state must be signals (or use
markForCheck()as a fallback) - Async operations like
setTimeoutand HTTP calls must update signals to trigger UI refreshes - Migration can be gradual: start with OnPush, convert to signals, then enable zoneless
- Performance gains include 35% smaller bundles, 87% fewer change detection cycles, and 33% faster startup
- Most major libraries (Material, NgRx, Ag Grid) already support zoneless mode
Key Takeaway
If you are starting a new Angular project in 2025 or 2026, go zoneless from day
one. Use ng new --zoneless and build your entire application with
signals as the primary state management primitive. For existing applications, plan a
gradual migration starting with the highest-traffic components.
In the next article of this series, we will explore the
New Template Syntax with @if, @for,
@switch, and @defer blocks, which replace the legacy
structural directives *ngIf, *ngFor, and ngSwitch
with a more powerful and readable syntax.
Related deep dive: 1 - Angular Signals: da Zero a Produzione.
Related deep dive: 3 - I Nuovi Template Angular: @if, @for, @defer.
Frequently asked questions
What is Zone.js and why is it being replaced in Angular?
Zone.js is a JavaScript library that intercepts virtually every asynchronous browser API to notify Angular when asynchronous operations start and complete. It was revolutionary when Angular 2 was released, but its convenience came with serious trade-offs that became increasingly problematic as applications grew in complexity.
How does the traditional change detection cycle work in Angular?
In the traditional Angular model, change detection follows a well-defined cycle tightly coupled to Zone.js. The critical problem is that every component and binding are checked, even if only a single variable in a single component actually changed, resulting in unnecessary equality checks.
What is the new zoneless change detection model in Angular?
The zoneless change detection model relies on Signals to know precisely which values have changed and which components need to be updated. Instead of intercepting async operations and checking everything, Angular now uses a notification system built on Angular's signal primitives.
How can I configure a fully zoneless application in Angular?
Setting up a zoneless Angular application involves three main steps: providing the zoneless change detection strategy, removing Zone.js from your polyfills, and updating your build configuration.
Related deep dive: OnPush e Signals come Default, non Eccezione: 1215 Componenti in un Monorepo Angular 21.
Related deep dive: Strict Mode non Basta: 100 Breach di Tipo in un Monorepo TypeScript al 100% Strict.
Commenti
Caricamento commenti...
Sign in to comment