DEV Community

Cristian Sifuentes
Cristian Sifuentes

Posted on

Angular 20: Going Zoneless — The Future Without ZoneJS

Angular 20: Going Zoneless — The Future Without ZoneJS

TL;DR — Angular 20 brings Zoneless mode, a revolution that eliminates ZoneJS entirely. This makes apps faster, lighter, and easier to debug. With Signals, OnPush, and render hooks like afterNextRender, 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()]
});
Enter fullscreen mode Exit fullscreen mode

NgModule bootstrap

@NgModule({
  providers: [provideZonelessChangeDetection()]
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

That’s it — your Angular app is now zoneless. 🎉


Removing ZoneJS

To truly go zoneless, remove ZoneJS from your build:

npm uninstall zone.js
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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 by AsyncPipe automatically)
  • 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); }
}
Enter fullscreen mode Exit fullscreen mode

With Signals, OnPush feels natural — every state change automatically marks the component for update.


Removing NgZone APIs

Once zoneless, remove usage of these:

  • NgZone.onMicrotaskEmpty
  • NgZone.onStable
  • NgZone.onUnstable
  • NgZone.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'));
  }
}
Enter fullscreen mode Exit fullscreen mode

💡 Need to react to DOM changes? Use MutationObserver for structural updates or afterNextRender for 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
});
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

Manual pending task tracking

const tasks = inject(PendingTasks);
const cleanup = tasks.add();
try {
  await loadData();
} finally {
  cleanup();
}
Enter fullscreen mode Exit fullscreen mode

RxJS helper

readonly items$ = this.http.get('/api/items').pipe(pendingUntilEvent());
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Prefer fixture.whenStable() over fixture.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)

Collapse
 
shemith_mohanan_6361bb8a2 profile image
shemith mohanan

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. 🚀