Zone.js was Angular's original sin. For years, it was the answer to a question nobody wanted to ask: "how does Angular know when to update the DOM?" The answer — monkey-patching every async API in the browser — was clever in 2016 and increasingly painful ever since. With Angular 20.2 shipping provideZonelessChangeDetection() as stable and Angular 21 making zoneless the default, Zone.js is finally, mercifully, optional. The Angular community is treating it less like a loss and more like a tumor removal.

What Zone.js Actually Did

For the uninitiated (or those who blocked it out): Zone.js wraps every setTimeout, Promise, addEventListener, XMLHttpRequest, and a pile of other browser APIs. Every async operation in your app goes through its monkey-patched versions. When any of them completes, Angular goes "something async happened — better check everything."

That's the entire change detection model. Something happened somewhere, so re-check the whole component tree. OnPush mitigated this, but you had to opt into it per-component, and plenty of teams just... didn't. The result was apps where a setTimeout in some utility service triggered change detection across hundreds of components that had nothing to do with the update.

The library also weighed in at roughly 33KB raw. Not catastrophic on its own, but it had to load before your app even bootstrapped. You can't lazy-load a polyfill that patches the entire runtime. Every Angular app paid this tax upfront, regardless of complexity.

How the Zoneless Model Works

The replacement is embarrassingly straightforward. Instead of "intercept everything and check everywhere," Angular now only re-renders when something explicitly announces a change:

  • Signals — read one in a template, and when it updates, Angular marks only that component dirty

  • AsyncPipe — still works; it calls markForCheck() under the hood

  • Event handlers — click, input, submit — trigger detection on the component where they fire

  • Manual callsmarkForCheck() and detectChanges() remain as escape hatches

No global interception. No tree walks triggered by unrelated setTimeout calls. Here's the bootstrap in Angular 20:

// app.config.ts
import { provideZonelessChangeDetection } from '@angular/core';

export const appConfig = {
  providers: [
    provideZonelessChangeDetection(),
    // ...other providers
  ]
};

A signal-based component looks like this:

@Component({
  selector: 'app-counter',
  template: `
    <p>Count: {{ count() }}</p>
    <button (click)="increment()">+1</button>
  `
})
export class CounterComponent {
  count = signal(0);
  increment() { this.count.update(n => n + 1); }
}

When count changes, Angular re-renders CounterComponent and nothing else. If you've spent time with Solid.js or Vue's composition API, this reactivity model will feel instantly familiar. Angular just took a decade longer to arrive.

The Migration Isn't Free

Dropping the provider and uninstalling the package is the easy part. The real work hides in code that accidentally depended on Zone.js's behavior.

Reactive Forms need rewiring. Calls to setValue(), patchValue(), and FormArray.push() don't trigger change detection in zoneless mode. If your forms aren't backed by signals, you'll need to manually connect observables to markForCheck() — or better, start migrating form state to signals entirely.

NgZone API references are everywhere in enterprise apps. NgZone.onMicrotaskEmpty, NgZone.onUnstable, NgZone.onStable — all deprecated. The replacements are afterNextRender() and afterEveryRender(), which are cleaner but require touching every call site. Angular ships a migration schematic that helps:

ng generate @angular/core:onpush_zoneless_migration

It analyzes your codebase and produces a recommended plan, but expect manual cleanup in anything over 50 components. Third-party libraries that relied on Zone.js's patching for timing logic may also need updates — most maintained packages have adapted by now, but audit your dependency tree before flipping the switch.

The actual removal steps are blissfully short:

# Remove zone.js from polyfills in angular.json (build + test targets)
# Delete:  import 'zone.js';  from polyfills.ts
npm uninstall zone.js
# Add provideZonelessChangeDetection() to your app config

Performance: The Numbers That Matter

Benchmarks across enterprise apps tell a consistent story:

Metric With Zone.js Zoneless Change
Bundle size +33KB Removed -33KB shipped to every user
Rendering speed Baseline 30–40% faster Targeted signal updates vs. full tree
Initial load Baseline ~12% faster No eager Zone bootstrap
LCP (vs. React 19) N/A 2.3s vs 2.3s Effectively tied

That last row deserves a pause. Angular matching React on Largest Contentful Paint was not happening two years ago. The combination of zoneless rendering and signal-based reactivity closed a gap that many developers assumed was permanent.

Google's own teams have reported concrete wins: migrating Gmail's Angular components to zoneless cut Time to Interactive by 400ms on mobile. That's not a synthetic benchmark — that's the world's most-used email client.

What This Actually Means

Every major frontend framework is converging on the same reactivity primitive: fine-grained, signal-based, no wasted work on things that didn't change. Vue had it with ref(). Solid was born on it. Svelte compiles down to it. React is working toward it — the compiler helps, but reconciliation is still the engine underneath.

Angular arriving at this model matters more than it would for a greenfield framework, because Angular is what enterprise teams run in production. Millions of lines of code across banking, healthcare, government, and logistics. For those teams, "stuck with Angular" is about to feel a lot less like a compromise and a lot more like a reasonable choice.

If you left during the Zone.js era, I'm not saying come back. But you might want to take another look at what shipped while you were gone.