Angular went zoneless and I'm never going back
Zone.js is finally optional. Zoneless change detection is stable, signals drive the updates, and my Angular apps got faster, smaller, and way easier to debug. Here's what actually changed.
For most of Angular's life, the thing actually deciding when your UI updated was a library you never imported on purpose: zone.js. It monkey-patched setTimeout, addEventListener, Promise, XHR — basically every async API in the browser — so that after anything async happened, Angular could run change detection over the whole component tree and figure out what moved.
It worked. It also meant your framework was globally rewriting the runtime, your stack traces went through zone.js frames nobody could read, and Angular re-checked the entire tree on a keystroke because it had no idea what actually changed. As of v20.2 zoneless change detection is stable, and in v21 zone.js is no longer in a new app by default. This is the update I'd been waiting years for, and ngl it's even better than I hoped.
What "zoneless" actually means
Without zone.js, Angular stops guessing. Instead of "some async thing happened, re-check everything," the rule becomes: a component is checked when something it depends on tells Angular it changed. That signal — literally — comes from signals, from markForCheck, from async pipe emissions, from template event bindings. The framework only does work when there's a real reason to.
You opt in with one provider and you delete zone.js from your polyfills:
import { bootstrapApplication } from '@angular/platform-browser';
import { provideZonelessChangeDetection } from '@angular/core';
bootstrapApplication(App, {
providers: [
provideZonelessChangeDetection(),
],
});That's the whole ceremony. New v21 apps scaffold this for you; on an existing app you add the provider, drop the zone.js import, and start fixing the handful of places that secretly relied on it.
Why it feels different
Three things changed for me immediately, and none of them are micro- benchmarks:
- Stack traces are mine again. When something throws in an event handler, the trace goes from my template to my code. No more scrolling past
zoneAwarePromiseandZoneTask.invokeframes trying to find the one line I wrote. - The bundle got smaller.
zone.jsis ~13 KB gzipped that every user downloaded and every app paid for at startup to patch the entire runtime. Deleting it is free performance on first load. - Updates are surgical. Type in an input on a page with a heavy list and Angular no longer walks the whole tree on every keystroke. With signals it checks the components that read the signal that changed. That's it.
The OnPush muscle memory you can throw away
For a decade the senior-Angular move was sprinkling ChangeDetectionStrategy.OnPush everywhere and being very careful about immutability so the framework would skip work. Zoneless + signals makes most of that ceremony obsolete. A signal-driven component is already only re-rendering when its signals change — that's the whole point of OnPush, except now it's the default behavior instead of a thing you remember to annotate.
@Component({
selector: 'app-counter',
template: `
<button (click)="count.set(count() + 1)">
clicked {{ count() }} times
</button>
<p>doubled: {{ doubled() }}</p>
`,
})
export class Counter {
count = signal(0);
doubled = computed(() => this.count() * 2);
}No ChangeDetectorRef, no OnPush ritual, no async pipe gymnastics. The click updates the signal, the signal tells Angular exactly which views read it, those views update. This is what reactive UI was supposed to feel like the whole time.
The catch — what breaks when you drop zone.js
I'd be lying if I said it's free. The things that break are exactly the things that were secretly leaning on zone.js to trigger a re-render for them:
- Mutating state in a raw
setTimeoutor a third-party callback and expecting the view to notice. Without zone.js nothing tells Angular to look. The fix is to put that state in a signal — which is what you wanted anyway. - Old libraries that assumed zone.js was global. Most modern ones are fine; a few ancient ones that manually poked Angular's change detection need a wrapper or a
signalbridge. - Tests that used
fakeAsync/tick()lean on zone.js semantics. Zoneless testing utilities exist now, but a big legacy test suite is real migration work, not a flip of a flag.
The honest framing: zoneless doesn't ask you to learn something hard, it asks you to stop relying on a magic trick. Every fix pushed me toward signals, which made the code more explicit and easier to reason about. That's a migration where the "cost" is just "your code gets better."
The boring conclusion
Zoneless Angular is the rare framework change that's simultaneously faster, smaller, easier to debug, and conceptually simpler. The mental model goes from "the framework re-checks everything after any async event and I annotate components to opt out" to "things update when their inputs change." That's not an Angular idea, that's just how reactivity should work — Angular finally shipped it as the default. If you're starting a new app in 2026, there's no reason to ship zone.js. If you're on an older one, do the migration. The version of your app on the other side is the one you actually wanted to be writing.