Angular Zone.js Explained — Understanding Async Context
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
NgZoneservice 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 detectionrunOutsideAngular()- Execute code outside the zoneonStable- 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
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.
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().Using NgZone for simple optimization — For most apps, the default zone behavior is fine. Only optimize when you measure a performance problem.
Not cleaning up outside-zone timers — Timers created outside the zone still need cleanup in
ngOnDestroy.Assuming zoneless is production-ready in older Angular versions — Zoneless was experimental before Angular 19. Check your Angular version documentation.
Practice Questions
What does Zone.js do in Angular? It intercepts async browser APIs and signals Angular to run change detection when async operations complete.
Why run code outside Angular's zone? To prevent unnecessary change detection on high-frequency events like scroll, animation frames, or WebSocket messages.
How do you safely update the UI from outside the zone? Wrap the UI update code in
ngZone.run(() => { ... }).What is the
onStableevent? An observable that emits when Angular's zone has no pending microtasks, meaning change detection is complete.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
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