Zone-Free Angular: Unlocking High-Performance Change Detection With Signals and Modern Reactivity
Angular v21 ditches Zone.js signals trigger rendering directly, toSignal() bridges Observables, markForCheck() fills the gaps.
Join the DZone community and get the full member experience.
Join For FreeAngular’s move toward zoneless change detection is a change in scheduling semantics rather than a removal of change detection. Instead of using Zone.js to infer that a render pass might be needed whenever certain asynchronous work completes, Angular schedules change detection from explicit framework notifications and from reactive state updates that Angular can track. The Angular performance guide states that zoneless is the default in Angular v21+, and it documents provideZonelessChangeDetection() as the bootstrapping hook used to enable zoneless scheduling in Angular v20.
Why Zoneless Became the Default
Angular’s official guidance frames Zone.js as a source of unnecessary synchronization. Zone.js uses DOM events and async tasks as indicators that the application state might have updated and triggers application synchronization to run change detection, while lacking insight into whether the state actually changed, so synchronization is triggered more frequently than necessary. The same guidance connects Zone.js to payload and startup overhead, debugging friction, and ecosystem compatibility risks that arise from patching native APIs, including the explicit note that some APIs cannot be patched effectively, such as async/await, which must be downleveled to work with Zone.js.
Angular’s v21 release announcement describes the maturity path behind the default, positioning zoneless change detection as progressing from experimental availability in v18 through stabilization in v20.2 and then becoming the default in v21, with zone.js and its features no longer included by default in Angular applications. The same announcement lists expected outcomes such as better Core Web Vitals, ecosystem compatibility, reduced bundle size, easier debugging, and better control over when change detection runs.
The Zoneless Notification Contract
Zoneless mode replaces patch-driven inference with an explicit notification surface. The provideZonelessChangeDetection() API documents configuring Angular not to use Zone.js state changes to schedule change detection and states that this works whether Zone.js is absent or present because another library depends on it. The same API documentation enumerates which notifications schedule change detection in a zoneless runtime, including ChangeDetectorRef.markForCheck(), ComponentRef.setInput(), updating a signal read in a template, triggers from bound host or template listener callbacks, attaching a dirty view, removing a view, and registering a render hook.
The zoneless performance guide reinforces the same contract and connects it to code patterns used in real applications. Angular relies on notifications from core APIs to determine when to run change detection and on which views, and it calls out that AsyncPipe is an important compatibility mechanism because it calls markForCheck() automatically. The same guide recommends OnPush as a step toward zoneless compatibility and documents removing Zone.js from builds by adjusting polyfills configuration for both build and test targets and uninstalling the dependency.
bootstrapApplication(AppComponent, {
providers: [provideZonelessChangeDetection()],
});
Angular also documents an explicit opt-in back to zone-based scheduling when required. The provideZoneChangeDetection() API is described as enabling NgZone/Zone.js-based change detection and as supporting configuration such as eventCoalescing, which can matter when dependencies still assume the older scheduler or when existing runtime behavior must remain stable while migration proceeds incrementally.
Signals as Modern Reactivity for Targeted Updates
Signals make the notification surface usable for everyday UI state. Angular documents writable signals as getter functions and documents that template rendering is a reactive context in which Angular monitors signal reads to establish dependencies. The signals guide also documents computed signals as lazily evaluated and memoized read-only derivations, with dynamic dependency tracking based on which signals are actually read during evaluation. In a zoneless runtime, this model aligns directly with the scheduling contract because updating a signal read in a template is itself a documented change detection trigger.
A minimal component sketch illustrates how event notifications and signal updates align with zoneless scheduling. A click handler is a bound template listener callback and, therefore, a documented scheduling trigger, and it updates a writable signal consumed by the template, which is another documented trigger. Pairing this with OnPush aligns with Angular’s recommendation for zoneless compatibility and reduces reliance on incidental global checks.
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<button (click)="increment()">+</button>
<span>{{ count() }}</span>
<span>{{ doubled() }}</span>
`,
})
export class CounterComponent {
readonly count = signal(0);
readonly doubled = computed(() => this.count() * 2);
increment() {
this.count.update((v) => v + 1);
}
}
Signals also make certain correctness constraints more visible because fewer incidental change detection passes exist to hide missing notification paths. The signals guide explicitly warns that readonly signals do not prevent deep mutation of their value and documents that the reactive context is only active for synchronous code, meaning signal reads after an asynchronous boundary are not tracked as dependencies. It also documents untracked() as a tool for preventing incidental dependency edges inside computed() and effect(), which becomes increasingly important as signal graphs grow in size and complexity.
Interop, SSR Stability, Forms, and Test Behavior
Angular’s RxJS interop completes the signals in templates approach for Observable-based services. The toSignal() API is documented as subscribing to an Observable and returning a signal that provides synchronous access to the most recent emitted value, throwing if the Observable errors. The RxJS interop guide adds operational constraints that frequently matter during zoneless migration: toSignal() subscribes immediately (similar to the async pipe), automatically unsubscribes when the creating component or service is destroyed, and should not be called repeatedly for the same Observable.
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `{{ user()?.displayName ?? 'Loading…' }}`,
})
export class UserBadgeComponent {
readonly user = toSignal(inject(UserService).user$, { initialValue: null });
}
Zoneless scheduling also changes how application stability and model-driven subsystems must communicate with rendering. Angular’s guide states that SSR has relied on Zone.js to determine when an application is stable enough to serialize and documents using the PendingTasks service to make Angular aware of asynchronous work that should delay serialization in a zoneless runtime, including the pendingUntilEvent helper for Observables. The same guide calls out reactive forms: model updates such as setValue, patchValue, and similar APIs emit forms observables but do not automatically schedule component change detection, so the recommendation is to connect forms observables to a change detection notification (for example markForCheck()) or reflect the relevant state through signals consumed by templates.
The guide also documents that TestBed uses Zone-based change detection by default when zone.js is loaded via polyfills, describes forcing zoneless behavior in tests by adding provideZonelessChangeDetection(), recommends minimizing fixture.detectChanges() when the goal is to validate real notification paths, and points to debug support via provideCheckNoChangesConfig({ exhaustive: true, interval: <milliseconds> }).
Conclusion
Zone-free Angular replaces patch-driven inference with an explicit notification surface and a reactive state model that Angular can track at the template boundary. Primary sources describe how Zone.js-driven inference triggers synchronization more often than necessary because async activity does not reliably correlate with state changes, and they also describe patching overhead and a maintenance posture that limits further patch expansion as Angular shifts away from Zone.js.
Zoneless scheduling makes rendering causes explicit and predictable, and signals plus RxJS interop utilities such as toSignal() provide the production-facing primitives needed to keep UI updates fast, targeted, and sustainable as application scale and async complexity increase.
Opinions expressed by DZone contributors are their own.
Comments