Why Zoneless Matters

Angular 19 lets you drop Zone.js entirely. That’s a big deal.

Zone.js is Angular’s change detection watchdog — it monkey-patches every async API (setTimeout, Promise, DOM events) to know when something might have changed, then checks the entire component tree. It works, but it’s expensive.

Going zoneless means Angular only re-renders when you explicitly tell it something changed. The result: up to 40% faster rendering in real-world apps.

Setup

Three steps. Five minutes.

1. Create a new project

ng new my-fast-app
# Choose your preferred options when prompted

2. Enable zoneless change detection

src/app/app.config.ts
import { provideExperimentalZonelessChangeDetection } from "@angular/core";
 
export const appConfig = {
  providers: [
    provideExperimentalZonelessChangeDetection(),
    provideRouter(routes),
    provideClientHydration(),
  ],
};

3. Remove Zone.js from polyfills

angular.json
{
  "polyfills": [
    // "zone.js" — remove or comment this out
  ]
}

Verify it worked

Open your browser console and type window.Zone. If it returns undefined, Zone.js is gone.

Signals: The Replacement

Without Zone.js, Angular needs another way to know when data changes. That’s Signals — reactive primitives that notify Angular exactly which values changed and which components need updating.

Basic signal

src/app/counter.component.ts
import { Component, signal } from "@angular/core";
 
@Component({
  selector: "app-counter",
  template: `
    <h2>Count: {{ count() }}</h2>
    <button (click)="increment()">Add One</button>
  `,
})
export class CounterComponent {
  count = signal(0);
 
  increment() {
    this.count.update((n) => n + 1);
  }
}

Call count() to read, .set() to replace, .update() to transform. Angular tracks which templates read which signals and only re-renders those.

Computed values

Derived state that automatically stays in sync:

src/app/price.component.ts
@Component({
  template: `<p>Total: ${{ total() }}</p>`
})
export class PriceComponent {
  price = signal(10);
  quantity = signal(2);
  total = computed(() => this.price() * this.quantity());
}

total recalculates only when price or quantity change. No manual subscription management.

Effects

For side effects — things that should happen because a signal changed:

src/app/theme.component.ts
export class ThemeComponent {
  theme = signal("light");
 
  constructor() {
    effect(() => {
      document.body.classList.toggle("dark", this.theme() === "dark");
    });
  }
}

RxJS interop

Already using Observables? Convert them to signals with toSignal:

src/app/clock.component.ts
@Component({
  template: `<p>Time: {{ time() }}</p>`,
})
export class ClockComponent {
  time = toSignal(interval(1000), { initialValue: 0 });
}

Quick Reference

01 Use signal() for values that change over time
02 Use computed() for derived values — never recalculate manually
03 Use effect() only for side effects (DOM, logging, external sync)
04 Signals are synchronous — no async pipe needed in templates
05 Use toSignal() to bridge existing RxJS observables

Further Reading