TL;DR — Angular 20 brings Zoneless mode, a revolution that eliminates
ZoneJSentirely. This makes apps faster, lighter, and easier to debug. With Signals, OnPush, and render hooks likeafterNextRender, Angular no longer needs to patch browser APIs to track async tasks — change detection becomes explicit, predictable, and blazing fast.
Why Zoneless?
The Zoneless revolution simplifies Angular’s core runtime by removing ZoneJS. Instead of monkey-patching async APIs, Angular 20 relies on signals, change detection notifications, and render callbacks.
Benefits
✅ Performance boost — no more global async tracking.
✅ Better Core Web Vitals — smaller payload, faster startup.
✅ Cleaner stack traces — easier debugging without Zone pollution.
✅ Future‑proof compatibility — no patching required for new APIs.
✅ Improved SSR and testing — deterministic behavior in all environments.
Enabling Zoneless Mode
Standalone bootstrap
import { bootstrapApplication, provideZonelessChangeDetection } from '@angular/core';
bootstrapApplication(AppComponent, {
providers: [provideZonelessChangeDetection()]
});
NgModule bootstrap
@NgModule({
providers: [provideZonelessChangeDetection()]
})
export class AppModule {}
That’s it — your Angular app is now zoneless. 🎉
Removing ZoneJS
To truly go zoneless, remove ZoneJS from your build:
npm uninstall zone.js
In angular.json, delete zone.js and zone.js/testing entries from polyfills in both build and test targets.
If you use an explicit polyfills.ts, remove:
import 'zone.js';
import 'zone.js/testing';
After that, your bundle is lighter and startup faster — no ZoneJS runtime overhead.
What Replaces ZoneJS?
Angular now relies on explicit change detection triggers:
-
ChangeDetectorRef.markForCheck()(called byAsyncPipeautomatically) ComponentRef.setInput()- Updating a signal that’s read in a template
- Template or host event listeners
- Attaching a dirty view via
ViewContainerRef
The key idea: Angular only re-renders when you change something. It’s reactive and intentional.
OnPush + Signals = Zoneless Harmony
You don’t need OnPush, but it’s highly recommended.
@Component({
selector: 'app-counter',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<h2>Count: {{ count() }}</h2>
<button (click)="increment()">+</button>
`,
})
export class CounterComponent {
count = signal(0);
increment() { this.count.update(c => c + 1); }
}
With Signals, OnPush feels natural — every state change automatically marks the component for update.
Removing NgZone APIs
Once zoneless, remove usage of these:
NgZone.onMicrotaskEmptyNgZone.onStableNgZone.onUnstableNgZone.isStable
These no longer emit or change values. Instead, replace them with:
✅ afterNextRender() → runs once after the next render.
✅ afterEveryRender() → runs after every DOM render.
Example
import { afterNextRender, Component } from '@angular/core';
@Component({
selector: 'app-dashboard',
template: `<h1>Dashboard</h1>`,
})
export class DashboardComponent {
constructor() {
afterNextRender(() => console.log('DOM ready after render'));
}
}
💡 Need to react to DOM changes? Use MutationObserver for structural updates or
afterNextRenderfor Angular render cycles.
NgZone.run Still Works
You don’t have to remove NgZone.run() or NgZone.runOutsideAngular() — they still work for libraries that rely on it.
this.zone.runOutsideAngular(() => {
// Perform a heavy calculation without triggering change detection
});
SSR: Waiting Without ZoneJS
Angular 20 replaces ZoneJS stability tracking with a new service: PendingTasks.
Automatically track SSR tasks
const tasks = inject(PendingTasks);
tasks.run(async () => {
const data = await this.http.get('/api/profile').toPromise();
this.profile.set(data);
});
Manual pending task tracking
const tasks = inject(PendingTasks);
const cleanup = tasks.add();
try {
await loadData();
} finally {
cleanup();
}
RxJS helper
readonly items$ = this.http.get('/api/items').pipe(pendingUntilEvent());
This ensures SSR waits for your async logic before serializing HTML.
Testing Zoneless Components
Use provideZonelessChangeDetection() with TestBed:
TestBed.configureTestingModule({
providers: [provideZonelessChangeDetection()]
});
const fixture = TestBed.createComponent(MyComponent);
await fixture.whenStable();
Prefer
fixture.whenStable()overfixture.detectChanges()for real‑world test behavior.
✅ Debug mode safety
Use provideCheckNoChangesConfig({ exhaustive: true, interval: 3000 }) to catch undetected bindings in dev mode.
Expert Tips & Pitfalls
✅ Use ChangeDetectorRef.markForCheck() manually when working with external data sources.
✅ Prefer Signals + AsyncPipe for automatic detection.
✅ Avoid relying on microtasks for sync — explicit is better than implicit.
✅ Use afterNextRender() for animation or measurement post-DOM update.
✅ SSR: wrap long async work with PendingTasks.run() for stable rendering.
✅ Libraries: keep NgZone.run() when needed — don’t break compatibility.
Resources
🔥 Zoneless Angular is not the future — it’s the present. Drop ZoneJS, embrace Signals, and unlock maximum performance with full control.

Top comments (1)
Fantastic breakdown — this is the clearest explanation I’ve seen on how Angular 20 actually operates without ZoneJS. The combination of Signals, PendingTasks, and render hooks like afterNextRender() makes the entire lifecycle explicit and predictable. Love how you connected it to better debugging and SSR stability — this really feels like Angular’s “React moment” in terms of developer clarity. 🚀