Goodbye Zone.js ? Angular’s Zone-less Change Detection Explained

If you’ve worked with Angular before, you’ve probably heard of Zone.js or at least run into it while debugging some weird behavior. For years, it’s been the “magic” that made Angular automatically update your UI whenever something changed in your app.

But times are changing and Angular is evolving, one of the most exciting changes is the ability to ditch Zone.js completely. That’s right you can now run Angular apps without Zone.js.

Wait, What Does Zone.js Even Do?

Let’s take a step back.

One of the things that makes Angular feel so seamless is how it just knows when to update the UI. You update a variable inside a setTimeout, or after an HTTP request, and magically, the view reflects the new data. You don’t have to tell Angular, “Hey, something changed. update the screen!” It just happens.

// The template will be updated automatically
setTimeout(() => {
  this.title = 'Updated!';
});

Zone.js patches common async behaviors stuff like timeouts, promises, and events and watches for any operation that might lead to a UI change. When it spots one, it gives Angular a nudge: “You might want to check for updates now.”

But Zone.js doesn’t stop there.

Many developers use Zone.js without even knowing it. For example, if your app crashes and you see a global error handler catch it - yep!, Zone.js was probably involved. Or when you’re using server-side rendering, Zone.js helps Angular figure out when all your data fetching is done so it can flush the final HTML to the client.

So yeah, it’s doing more than just change detection. It's kind of like the invisible operator working behind the scenes, making sure Angular keeps everything in sync.

Why Did Angular Rely on Zone.js for So Long?

Zone.js was Angular’s secret sauce for years because it made updating the UI automatic and easy. Before it, you had to manually tell Angular when to check for changes which was a pain and easy to mess up.

Zone.js quietly watches all the async stuff happening in your app (like timers or HTTP calls) and tells Angular when to update the view. It made life way simpler for developers.

But the downside? It listens to everything, which can slow down big apps by running extra checks. That’s why Angular is now moving away from it, aiming for a faster, cleaner way to keep your UI in sync.

The New Way: Zone-less Angular

Zone-less Angular actually kicked off back in version 17, and since then, they’ve been polishing it bit by bit. Now, with Angular 20, turning off Zone.js is super simple all you have to do is add provideZonelessChangeDetection when you bootstrap your app:

import { provideZoneChangeDetection, provideBrowserGlobalErrorListeners } from "@angular/core";

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

And If you're using client rendering you can add provideBrowserGlobalErrorListeners  to have global error handling. While the SSR have a default error handler.

And be sure to delete zone.js from the angular.json file as well.

"architect": {
  "build": {
    "options": {
      "polyfills": ["zone.js"] // Delete me !
    }
  },
  "test": {
    "options": {
      "polyfills": ["zone.js", "zone.js/testing"] // Also Delete me !
    }
  }
}

That’s it. Now you’re responsible for letting Angular know when something actually changed.

Signals are your best friend

Think of a signal as a reactive variable. When its value changes, Angular knows exactly what depends on it and updates the view accordingly without needing any global patching or detection loops.

The best part? You don’t have to guess when to update or manually tell Angular to check for changes. Unlike Zone.js, which watches everything going on in the background and reacts to all async stuff, signals are way more focused and clear. They say, “Hey, this changed here, so just update that,” which keeps your app running smoother and faster.

If you’ve used React’s useState or other reactive hooks before, signals will feel familiar but they’re built right into Angular’s core. You just call the signal to get the current value, and Angular handles the rest quietly behind the scenes.

Using signals means your code stays cleaner, simpler, and easier to follow. Updates aren’t some mysterious magic trick anymore they’re straightforward and intentional, which makes your app easier to debug and maintain.

@Component({
  standalone: true,
  selector: 'app-counter',
  template: `
    <label> {{ count() }} * 2 = {{ double() }} </label>
    <button (click)="increment()">Count: {{ count() }}</button>
  `,
})
export class CounterComponent {
  count = signal(0);
  const double = computed(() => count() * 2);

  increment() {
    this.count.set(this.count() + 1);
  }
}

When you call increment(), the count signal updates, double recalculates, and any part of your template using either of them automatically re-renders.

No Zones. No ChangeDetectorRef. Just clean, declarative reactivity.

But How Did We Do This Before?

If you’ve built Angular apps the old way (with Zone.js), this is probably what your counter looked like:

@Component({
  selector: 'app-counter',
  template: `
    <label> {{ count * 2 }} </label>
    <button (click)="increment()">Count: {{ count }}</button>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CounterComponent {
  count = 0;

  constructor(private cdr: ChangeDetectorRef) {}

  increment() {
    this.count++;
    this.cdr.markForCheck(); // Manually tell Angular to check for updates
  }
}

It works, but it’s kind of clunky:

  • You have to remember to manually notify Angular.
  • You might end up calling markForCheck() in lots of places.
  • Zone.js still wraps everything async behind the scenes which can trigger more change detection cycles than necessary.

Why Signals Make Life Easier

Once you start using signals, it just clicks.

  • Updates are explicit, not magical.
  • Code is cleaner, because you don’t need all the boilerplate.
  • Angular only re-renders the parts that matter, which is great for performance.

Move Away from Zone.js But Why It Still Needs Time

The idea of ditching Zone.js sounds amazing better performance, cleaner reactivity, and more control over your app. And yes going zone-less is absolutely the future of Angular. But let’s be real as of today, we're not 100% there yet.

  • Some third-party library might still depend on zone.js
  • The Learning Curve is Real: using Signals require a shift in mindset and architecture.
  • Mixing the code (some parts with Zones, others without) can get messy.

What About Performance?

Going zone-less results in:

  • Faster startup times.
  • Less memory usage.
  • More predictable UI rendering.
  • Better compatibility with native browser features.

Also, removing Zone.js means Angular runs fewer checks overall, which is a big win for complex apps with lots of async interactions.

So what's the move?

If you are building a new app — go for it.

If you want to migrate an old app — take it slow, Zone-less Angular is the future. But like any transition, it needs time.

Conclusion

Moving away from Zone.js isn’t just a technical change it’s a mindset shift. It’s about making reactivity more intentional, cutting down on hidden behavior, and giving you better control over when and how your app updates.

Angular without Zone.js feels faster, cleaner, and more in tune with how modern frameworks work.

Give it a shot and see how it feels to build without the magic.

If you have a problem and no one else can help. Maybe you can hire the Kalvad-Team.