Zoneless Angular: provideExperimentalZonelessChangeDetection()
- Last updated
- Prerequisites:
- Angular change detection basics (OnPush, Default)
- Understanding of signals and Zone.js
- angular
- zoneless
- change-detection
- signals
- zonejs
- reactivity
Read at your depth
The practical view
Angular traditionally uses Zone.js: it monkey-patches browser APIs (setTimeout, addEventListener, Promise) so any async activity triggers change detection over the whole component tree. Zoneless mode removes Zone.js entirely: provideExperimentalZonelessChangeDetection() tells Angular to detect changes only when signal-based state (or explicit markForCheck) actually changes. With zoneless, your app no longer loads zone.js, async operations don't cause full-tree checks, and OnPush components refresh only when their inputs or read signals change. In Angular 18+ you can also use provideZonelessChangeDetection(); the API stabilized in later versions.
The same idea in other frameworks
React's granular re-renders are similar in spirit; Angular's zoneless CD is closer to Solid's fine-grained invalidation.provideExperimentalZonelessChangeDetection() — the current topic.Vue's effect-driven component updates mirror the signal-notification model of zoneless Angular.Svelte 5 runes compile to signal-driven updates; Angular zoneless is the same idea via runtime signals.Legacy vs modern
Zone.js full-tree CD vs zoneless signal-driven CD
With Zone.js, any async task checks the whole tree; with zoneless, only views reading changed signals are refreshed.
@Component(...)
export class App {
count = 0;
ngOnInit() {
setInterval(() => this.count++, 1000); // ❌ OLD: zone patches → full-tree CD every tick
}
}count = signal(0); // ✅ NEW: only views reading count() refresh
constructor() {
setInterval(() => this.count.update(c => c + 1), 1000);
}Interview gotchas
Context
Interviewers ask this to check whether you understand the implicit contract Zone.js provided — that ANY async activity refreshed the view — and what must become explicit without it.
The mechanical answer
Under Zone.js, setTimeout, Promise callbacks, DOM events, and HTTP responses all automatically mark the tree for CD. Zoneless removes that blanket trigger: if you mutate plain (non-signal) component state inside a setTimeout or a Promise callback without any signal involvement, the view will NOT refresh automatically. The fixes are: model state with signals (signal(), computed(), input()), use the async pipe for observables (which calls markForCheck internally on emission), or explicitly call ChangeDetectorRef.markForCheck()/detectChanges(). Also, code that relied on zone's error handling or that triggers CD from non-angular contexts (e.g. a standalone utility that mutates state) must now route through signal primitives. The build also drops zone.js, changing bundle size and polyfill assumptions.
Trap
The naive answer is 'nothing breaks, it's just faster'. The real trap is stale views: any async mutation of non-signal state silently stops updating the UI. Another trap: assuming the async pipe stops working — it works, but through its own notification path, not through zone. Candidates who enumerate the explicit notification paths (signals, async pipe, markForCheck) demonstrate real migration experience.
Context
This tests the pull-based notification graph — interviewers want the mechanics of how dirty views are identified and why OnPush-style thinking is now the default.
The mechanical answer
The template compiler and runtime track which views read which signals. When a signal is written, it bumps its version and notifies registered consumers — including the template bindings/effects that read it. Angular's scheduler maintains a dirty-views set; views that read changed signals are marked dirty, and change detection runs only for those views (plus ancestors up to an OnPush boundary to satisfy input propagation). This is essentially the OnPush model applied globally: a view is checked when an input changes, a signal it reads changes, an async pipe emits, or markForCheck is called. The key detail is that the signal→view edge is registered at render time, so the graph is exact, not an approximation.
Trap
The naive answer is 'zoneless checks the same tree but less often'. It checks a different SET — only views with signal edges to changed state. Another trap: assuming ancestor components with Default strategy always re-run — they only re-run if they themselves read a changed signal or receive changed inputs; otherwise they are skipped entirely. Candidates who describe the dirty-view set and signal-consumer edges show real understanding.