Skip to content

Angular Zone.js Explained — Understanding Async Context

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Angular Zone.js Explained. We cover key concepts, practical examples, and best practices to help you master this topic.

Zone.js is a library that intercepts asynchronous APIs in JavaScript, allowing Angular to know when async operations complete and automatically trigger change detection.

What You'll Learn

  • What Zone.js is and how it monkey-patches async APIs
  • How Angular uses zones to trigger change detection
  • When and why to run code outside Angular's zone
  • How to use NgZone service for zone management
  • How zoneless change detection works with signals

Why It Matters

Zone.js is the engine behind Angular's automatic change detection. Understanding it helps you optimize performance, avoid unnecessary change cycles, and build responsive apps that update only when needed.

Real-World Use

Durga Antivirus Pro processes file scan events on a Websocket. The WebSocket events run outside Angular's zone to avoid triggering change detection on every message. Only when a threat is detected does the code re-enter the zone and update the UI.

flowchart TD
    A[Browser Event] --> B[Zone.js Intercept]
    B --> C[Async Task Runs]
    C --> D{In Angular Zone?}
    D -->|Yes| E[Zone microtask]
    D -->|No| F[Task completes silently]
    E --> G[Angular CD Triggered]
    G --> H[DOM Updated]
    style A fill:#f97316,color:#fff

What is Zone.js?

Zone.js wraps every asynchronous browser API with a "zone" that tracks execution context:

// Zone.js patches these APIs automatically:
// - setTimeout, setInterval, setImmediate
// - Promise.then, Promise.catch
// - Event listeners (click, mouseover, etc.)
// - XMLHttpRequest, fetch
// - requestAnimationFrame
// - WebSocket

When a patched API completes, Zone.js notifies Angular. This is how Angular knows to run change detection without you explicitly calling any update method.

Running Code Outside the Zone

For performance-critical code, run outside Angular's zone to prevent unnecessary change detection:

import { Component, NgZone, OnInit, OnDestroy } from "@angular/core";

@Component({
  selector: "app-scroll-spy",
  standalone: true,
  template: `
    <div #scrollContainer style="height:300px;overflow-y:scroll" (scroll)="onScroll($event)">
      <div style="height:2000px;background:linear-gradient(180deg,#fff,#f97316)">
        <p style="position:sticky;top:0">Scroll position: {{ scrollPercent }}%</p>
      </div>
    </div>
  `
})
export class ScrollSpyComponent implements OnInit, OnDestroy {
  scrollPercent = 0;
  private rafId = 0;

  constructor(private ngZone: NgZone) {}

  ngOnInit() {
    this.ngZone.runOutsideAngular(() => {
      const updateScroll = () => {
        const scrollTop = window.scrollY || document.documentElement.scrollTop;
        const height = document.documentElement.scrollHeight - window.innerHeight;
        const percent = Math.round((scrollTop / height) * 100);
        // Only update Angular when value changes
        if (percent !== this.scrollPercent) {
          this.ngZone.run(() => {
            this.scrollPercent = percent;
          });
        }
        this.rafId = requestAnimationFrame(updateScroll);
      };
      this.rafId = requestAnimationFrame(updateScroll);
    });
  }

  onScroll(event: Event) {
    console.log("Scroll event in zone");
  }

  ngOnDestroy() {
    cancelAnimationFrame(this.rafId);
  }
}

Expected output: Smooth scrolling without jank. The scroll percentage updates in the Angular template only when the value changes, not on every animation frame.

The rafId runs entirely outside the zone, preventing Angular from running change detection 60 times per second. Only the ngZone.run() block triggers a check cycle.

NgZone Methods

The NgZone service provides several methods for zone management:

import { Component, NgZone } from "@angular/core";

@Component({
  selector: "app-zone-methods",
  standalone: true,
  template: `<p>Status: {{ status }}</p>`
})
export class ZoneMethodsComponent {
  status = "idle";

  constructor(private ngZone: NgZone) {
    console.log("Is Angular zone:", ngZone.isStable);
  }

  runInZone() {
    this.ngZone.run(() => {
      this.status = "Updated inside zone";
    });
  }

  runOutside() {
    this.ngZone.runOutsideAngular(() => {
      setTimeout(() => {
        console.log("This runs outside zone");
      }, 1000);
    });
  }

  onStable() {
    this.ngZone.onStable.subscribe(() => {
      console.log("Change detection completed");
    });
  }
}

Expected output: runInZone triggers change detection. runOutside does not. onStable fires after each change detection cycle completes.

  • run() - Execute code inside Angular's zone and trigger change detection
  • runOutsideAngular() - Execute code outside the zone
  • onStable - Observable that emits when the zone stabilizes (no pending microtasks)
  • isStable - Whether the zone has no pending async tasks

Measuring Zone Impact

Compare performance with and without Zone.js intervention:

import { Component, NgZone, ChangeDetectorRef } from "@angular/core";

@Component({
  selector: "app-perf-compare",
  standalone: true,
  template: `
    <p>In zone updates: {{ inZoneCount }}</p>
    <p>Outside zone updates: {{ outsideCount }}</p>
    <button (click)="startBoth()">Start Both Timers</button>
  `
})
export class PerfCompareComponent {
  inZoneCount = 0;
  outsideCount = 0;
  private inZone = 0;
  private outside = 0;

  constructor(private ngZone: NgZone, private cdr: ChangeDetectorRef) {}

  startBoth() {
    // Timer inside zone - triggers CD every 10ms
    this.ngZone.run(() => {
      setInterval(() => {
        this.inZoneCount++;
      }, 10);
    });

    // Timer outside zone - no CD trigger
    this.ngZone.runOutsideAngular(() => {
      setInterval(() => {
        this.outside++;
        if (this.outside % 100 === 0) {
          this.ngZone.run(() => {
            this.outsideCount = this.outside;
          });
        }
      }, 10);
    });
  }
}

Expected output: The in-zone timer updates the UI on every tick (every 10ms), potentially causing jank. The outside-zone timer batches updates, rendering only every 100th tick (every 1 second).

This demonstrates why intensive async work should run outside the zone, with selective re-entry for UI updates.

Zoneless Angular

Angular 17+ supports zoneless change detection with signals:

import { Component, signal } from "@angular/core";
import { bootstrapApplication } from "@angular/platform-browser";
import { provideExperimentalZonelessChangeDetection } from "@angular/core";

@Component({
  selector: "app-zoneless",
  standalone: true,
  template: `<p>Count: {{ count() }}</p>`
})
export class ZonelessComponent {
  count = signal(0);

  increment() {
    this.count.update(c => c + 1);
  }
}

// In main.ts:
// bootstrapApplication(AppComponent, {
//   providers: [provideExperimentalZonelessChangeDetection()]
// });

Expected output: The component updates without Zone.js. Signals notify Angular directly when values change, eliminating the need for zone patching.

Zoneless Angular is the future. It reduces bundle size (no Zone.js), improves performance (no monkey-patching), and gives more predictable change detection based on signal graph dependencies.

Common Mistakes

  1. Running all async code in the zone — Default Angular behavior keeps everything in the zone. This is safe but may cause performance issues with high-frequency events like scroll, mousemove, or WebSocket messages.

  2. Forgetting to re-enter the zone — If you run code outside the zone and need to update the UI, wrap UI updates in ngZone.run().

  3. Using NgZone for simple optimization — For most apps, the default zone behavior is fine. Only optimize when you measure a performance problem.

  4. Not cleaning up outside-zone timers — Timers created outside the zone still need cleanup in ngOnDestroy.

  5. Assuming zoneless is production-ready in older Angular versions — Zoneless was experimental before Angular 19. Check your Angular version documentation.

Practice Questions

  1. What does Zone.js do in Angular? It intercepts async browser APIs and signals Angular to run change detection when async operations complete.

  2. Why run code outside Angular's zone? To prevent unnecessary change detection on high-frequency events like scroll, animation frames, or WebSocket messages.

  3. How do you safely update the UI from outside the zone? Wrap the UI update code in ngZone.run(() => { ... }).

  4. What is the onStable event? An observable that emits when Angular's zone has no pending microtasks, meaning change detection is complete.

  5. How does zoneless change detection work? It uses signals and notify() to directly inform Angular when dependencies change, without needing Zone.js interception.

Challenge

Build a WebSocketPriceFeedComponent that connects to a simulated WebSocket emitting prices every 50ms. Process the feed outside Angular's zone. Only re-enter the zone when the price changes by more than 0.5%. Display the price, percent change, and an FPS counter showing how many times the UI actually updated.

FAQ

Does Zone.js affect all browsers equally?

Yes, Zone.js patches standard APIs. Legacy browsers (IE) may require additional polyfills.

Can I remove Zone.js from my Angular app?

Yes, with zoneless change detection in Angular 17+. Remove Zone.js and use signals with provideExperimentalZonelessChangeDetection.

What is the performance cost of Zone.js?

Zone.js adds minimal overhead per async operation (microseconds). The main cost is unnecessary change detection cycles, not the interception itself.

How does NgZone relate to Zone.js?

NgZone is a wrapper that extends Zone.js with Angular-specific features like onStable and isStable.

Does Zone.js patch all async operations?

It patches standard browser APIs. Custom event emitters or Web Workers are not patched by default.

Mini Project

Build a PerformanceDashboardComponent that monitors and displays change detection cycles per second. Create a high-frequency data stream (price updates every 10ms). Run it both inside and outside the zone, then compare the CD cycle count and UI responsiveness. Add controls to switch between strategies and display the FPS impact.

What's Next

Now that you understand zones, move on to modern Angular patterns:

Angular Signals, Angular Standalone, Angular Change Detection

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro