A while back we published two articles tracing the evolution of change detection in Angular.

First, we understood how Angular was able to "know" when to update the UI thanks to Zone.js, the component tree, and change detection strategies. Then we made the leap to Signals and saw how Angular began updating only what actually depended on reactive state.

In this third chapter we're going to close the loop. Because if Signals solved what needs to be updated… Zoneless completely changes when Angular decides to run change detection.

And yes: Angular can now run without Zone.js.

From "Angular detects everything" to "Angular detects only what's necessary"

In the first article we saw that Angular used Zone.js to intercept asynchronous operations:

  • Clicks
  • setTimeout
  • HTTP requests
  • Promises
  • Browser events…

Every time any of those operations occurred, Angular triggered a full change detection cycle. The problem is that Angular didn't actually know whether anything relevant had changed.

It simply assumed:

"Something asynchronous happened. Just in case… I'll check the whole application."

And that worked really well, but it also meant unnecessary work.

The clock example

Let's follow exactly the same example from the previous articles. We have:

  • A clock that updates every second
  • A list of users.
@Component({
 selector: 'app-root',
 template: `
   <h2>{{ clock }}</h2>

   @for (user of users; track user.id) {
     <app-user-row [user]="user"></app-user-row>
   }
 `
})
export class AppComponent {
 clock = '';

 users = USERS;

 ngOnInit() {
   setInterval(() => {
     this.clock = new Date().toLocaleTimeString();
   }, 1000);
 }
}

In the first article we saw that:

  • Every second, Angular traversed the entire tree
  • Recalculated bindings
  • Executed expressions
  • Checked every component

Even though the users hadn't changed.

OnPush was the first patch

Then came ChangeDetectionStrategy.OnPush.

@Component({
 changeDetection: ChangeDetectionStrategy.OnPush
})

With OnPush, Angular stopped checking components "just because," and only checked them when:

An @Input changed

  • An event occurred inside the component
  • An observable emitted via async
  • Or we manually called markForCheck()
    It was much more efficient, but we still depended on Zone.js because Angular still needed a global mechanism to say:
"Hey, something asynchronous just happened."

Signals changed the rules of the game

And this is where the big revolution arrived. With Signals, Angular no longer needs to "wonder" what changed, now it knows.

clock = signal('');
setInterval(() => {
 this.clock.set(new Date().toLocaleTimeString());
}, 1000);

In the template:

<h2>{{ clock() }}</h2>

Angular automatically registers which parts of the UI depend on each signal, and when the signal changes:

  • Angular marks only the affected components
  • It avoids traversing unnecessary branches
  • It updates only the dependent bindings

This already meant a huge performance leap, but there was still an important question left: if Signals already knows exactly what changes… why do we still need Zone.js?

This is where Zoneless comes in

The short answer is that we don't need it as much anymore. Angular started introducing support for "zoneless" applications, that is, applications without Zone.js, and that completely changes the mental model.

What does Zone.js actually do?

Zone.js patches browser APIs to intercept asynchronous tasks:

  • Timers
  • Events
  • Promises
  • XHR
  • Fetch
  • etc.

Whenever any of those tasks finishes, Angular runs global change detection. The problem is that this comes with costs:

  • More unnecessary work
  • More detection cycles
  • Worse startup
  • Harder-to-read stack traces
  • And more internal complexity

In fact, one of the main goals of Zoneless is to improve performance, Core Web Vitals, compatibility with modern APIs, and the debugging experience.

How does Angular work without Zone.js?

In zoneless mode, Angular stops "spying" on the entire browser. Instead, it only updates the UI when it receives explicit notifications. For example:

  • A signal changes,
  • An AsyncPipe emits
  • An Angular event occurs ((click))
  • markForCheck() is called
  • An @Input receives a new value

In other words: Angular no longer does implicit polling of state. Now the framework itself knows exactly when something relevant has changed, and here lies probably the most important idea of this whole shift: Angular moves from an implicit reactive model to an explicit one.

Enabling Zoneless

Currently, Angular lets you enable it via:

bootstrapApplication(AppComponent, {
 providers: [
   provideZonelessChangeDetection()
 ]
});

And removing:

npm uninstall zone.js

Back to the clock example

Now, our example changes quite a bit.

@Component({
 selector: 'app-root',
 template: `
   <h2>{{ clock() }}</h2>
   @for (user of users(); track user.id) {
     <app-user-row [user]="user"></app-user-row>
   }
 `})
export class AppComponent {
 clock = signal('');
 users = signal(USERS);

 ngOnInit() {
   setInterval(() => {
     this.clock.set(
       new Date().toLocaleTimeString()
     );
   }, 1000);
 }
}

What happens now every second?

  • Only clock changes
  • Angular marks only that binding
  • The user table doesn't even enter the cycle.

That "check the whole application just in case" no longer exists.

So… does Signals replace OnPush?

Not exactly. Signals and Zoneless greatly improve how Angular schedules change detection, but OnPush is still important.
Because Zoneless doesn't change how Angular traverses the component tree; what changes is when it decides to trigger change detection.

So:

  • OnPush still helps limit checks
  • Signals still marks specific components
  • Zoneless avoids triggering unnecessary global cycles.

The three pieces complement each other, and in fact, the official documentation itself recommends OnPush as a natural step toward zoneless compatibility.

Something important: Angular still detects events

There's a very interesting detail here. Even if we remove Zone.js, this still works:

<button (click)="increment()">
 Increment
</button><br>
counter++;

Why? Because events registered through Angular still notify the framework automatically.

But be careful: this does NOT happen with APIs outside the Angular ecosystem. For example:

element.addEventListener('click', () => {
 this.counter++;
});

Here, Angular no longer knows that something changed, and we would need:

markForCheck()

Or use Signals.

What starts to "break" in Zoneless

This is where the theory gets interesting, because most current Angular applications indirectly depend on Zone.js's automatic behaviors.
And when we remove it… certain surprises show up. For example, this classic pattern stops working correctly:

this.userService.users$
 .subscribe(users => {
   this.users = users;
 });

If we then display users directly in the template, Angular might not notice the change, because the subscription happens outside any reactive mechanism Angular can observe.

The modern solution involves:

  • Using the async pipe
  • Converting observables to signals
  • Explicitly calling markForCheck().

For example:

users = toSignal(this.userService.users$);

Here you can clearly start to see where Angular wants to go: less implicit magic and more explicit reactivity.

Another important detail is Reactive Forms. Operations like form.patchValue(...) still update the form's internal state… but no longer automatically force change detection.

Since which version has Zoneless existed?

Angular has been evolving this capability over several versions.

Version Status
Angular 17.1 First experimental internal APIs (ɵprovideZonelessChangeDetection)
Angular 18 Official experimental support via provideExperimentalZonelessChangeDetection()
Angular 20.2 Stable API provideZonelessChangeDetection()
Angular 21+ Zoneless becomes the default behavior

The real paradigm shift

Angular is leaving behind a "check just in case" model to move to a model based on explicit reactivity.

Much more predictable, much more efficient, and quite a bit closer to how modern frameworks like Solid or Vue Signals work.

Stage What happened
Classic Angular Angular constantly checks everything
OnPush Angular checks fewer components
Signals Angular knows exactly what changed
Zoneless Angular knows exactly when to react

Is it ready for production?

As of today, Zoneless is already part of Angular's official strategy, and the framework is clearly oriented toward this reactive model, but that doesn't mean any application can just remove Zone.js tomorrow without further thought. It's worth carefully validating:

  • Third-party libraries
  • Manual DOM integrations
  • Legacy code
  • Reactive forms
  • SSR
  • Testing
  • Patterns based on implicit side effects

In fact, Angular strongly emphasizes that the future lies in Signals, the async pipe, OnPush, reactive APIs, and explicit notifications to the framework.

Conclusions

For years, Angular relied on Zone.js to detect any possible change in the application. Then came OnPush to reduce unnecessary work. Later, Signals appeared to enable much more precise reactivity.

And now Zoneless finishes closing that evolution by removing the need to constantly monitor everything happening in the browser.

The combination of Signals, OnPush, and Zoneless lets us build much more efficient, predictable, and easy-to-reason-about applications.

But it also requires a much better understanding of how the framework's reactivity actually works. Because Angular no longer tries to guess what's happening; now it expects us to be explicit, and that's probably the most important change of all.

References

Previous articles in the series

Official Angular documentation

Technical articles and analysis

Tell us what you think.

Comments are moderated and will only be visible if they add to the discussion in a constructive way. If you disagree with a point, please, be polite.