All insights

Architecture

Angular Zoneless Change Detection: A Practical Migration Guide

Going zoneless is the final step, not the first. Make Angular state changes explicit, fix async boundaries and migrate incrementally before removing Zone.js.

Zone.js global change detection compared with explicit Angular Signals updating only dependent components.

Enabling zoneless change detection is one of the smallest parts of a zoneless Angular migration. The difficult part is finding the places where the application currently depends on Zone.js to notice that something changed.

The migration becomes much safer when zoneless is treated as a property the application earns rather than a flag that gets enabled first.

Why a big-bang Angular zoneless migration is risky

Before and after comparison of Zone.js global change detection and explicit signal notification in Angular.
Zone.js checks the whole component tree; a signal notifies only the components that read it.

A long-lived migration branch quickly diverges from active feature development. Every feature merged into main creates another conflict with code that may already have been converted on the migration branch.

A safer approach is to make each preparation step independently useful and merge it directly into the application while Zone.js is still present.

Step 1: audit your Angular Zone.js dependencies

Search for common places where Angular code may depend on implicit Zone.js change detection.
rg -n "setTimeout|setInterval" src --type ts rg -n  "NgZone|runOutsideAngular|ApplicationRef.tick"  src --type ts rg -n  "detectChanges|markForCheck|ChangeDetectorRef"  src --type ts rg -n  "addEventListener|new Worker|WebSocket|IntersectionObserver"  src --type ts

Classify the results instead of automatically replacing them. Some are state mutations that should become explicit signal writes. Some are legitimate work outside Angular. Others are manual change-detection calls that reveal an existing notification problem.

Step 2: convert change-detection triggers to Signals

The useful migration boundary is the source of the asynchronous state change rather than the component consuming it. Once a timer or callback writes to a signal, every consumer of that signal has an explicit notification mechanism.

A Signal makes the state update itself the notification.
export class SessionBannerComponent {
  readonly remaining = signal(0);
  private readonly session = inject(SessionService);
  constructor() {
    const id = setInterval(() => {
      this.remaining.set( this.session.secondsLeft() );
    }, 1000);
    inject(DestroyRef).onDestroy(() => {
      clearInterval(id);
    });
  }
}

Step 3: handle the Angular RxJS boundary correctly

Angular async sources flowing through an RxJS pipeline and converting to signals at an explicit toSignal boundary.
RxJS stays where the problem is a stream; toSignal marks the single boundary where it becomes template state.

Zoneless Angular does not mean removing RxJS. Search debouncing, cancellation, WebSockets and merged asynchronous sources are still stream problems.

The important distinction is the boundary where an observable result becomes component state. Converting that boundary to a signal makes the notification explicit.

Use toSignal when an RxJS stream becomes state consumed by the Angular template.
export class CaseListComponent {
  private readonly api = inject(CaseApi);
  readonly cases = toSignal( this.api.cases(), {
    initialValue: [] as Case[],
  } );
}

Step 4: audit third-party Angular libraries

Third-party libraries often determine the real migration timeline. Charting libraries, map SDKs, older UI components and browser integrations may invoke callbacks outside Angular.

At those boundaries, update application state explicitly rather than depending on the zone to observe the callback.

A Signal write makes a third-party callback visible to Angular without Zone.js.
export class ChartHostComponent {
  readonly selectedPoint = signal<Point | null>(null);
  private init(el: HTMLElement): void {
    thirdPartyChart(el, {
      onSelect: (point: Point) => {
        this.selectedPoint.set(point);
      },
    });
  }
}

Step 5: enable Angular zoneless change detection

A five-step incremental Angular zoneless migration: audit, signals, RxJS boundary, library audit, then zoneless.
Each step is useful on its own and can merge into main while Zone.js is still enabled.

Once application state changes are explicit and third-party boundaries have been audited, enabling zoneless change detection becomes the final verification step rather than the beginning of the migration.

Enable zoneless change detection after the application has been prepared for it.
bootstrapApplication(AppComponent, {
  providers: [ provideZonelessChangeDetection(), provideBrowserGlobalErrorListeners(), ],
});

Remove Zone.js from both application and test polyfills. Keeping it in the test environment can hide exactly the notification problems the migration is intended to expose.

The real benefit of zoneless Angular

The strongest reason to migrate is not a single benchmark. Zoneless architecture encourages precise state ownership and explicit notification boundaries.

Most of that architectural value appears before Zone.js is removed. If each migration step improves the running application independently, the project remains safe to pause, ship and continue without a branch that becomes increasingly difficult to merge.

Sources

Where this is applied

Keep reading