| |

Angular 64 🅰️ Zone.js and Change Detection Internals

The previous chapter covered the developer-facing tools for change detection: the ChangeDetectorRef, the signals, and the markForCheck(). This chapter goes underneath those tools to the machinery that drives them. For most of Angular’s history, that machinery has been Zone.js — a library that patches the browser’s asynchronous APIs and tells Angular when something might have changed. The NgZone service is Angular’s wrapper around it, and the onMicrotaskEmpty event is the trigger that kicks off a change detection cycle. Understanding this internals matters because the framework is migrating away from Zone.js toward the zoneless model, and the migration only makes sense if you know what Zone.js was actually doing. This chapter covers the Zone.js’s patching mechanism, the NgZone‘s and the ZoneImpl‘s roles, the onMicrotaskEmpty‘s trigger, the runOutsideAngular‘s and the run‘s, the zone pollution, the async/await‘s gotcha, the zoneless’s replacement, and the patterns that make the transition safe.

Key point: Zone.js works by monkey-patching the native browser and Node.js APIs — setTimeout, setInterval, Promise, addEventListener, XMLHttpRequest, and fetch — so that every asynchronous task runs inside a “zone” that tracks its start and completion . Angular’s NgZone listens to the microtask queue that Zone.js maintains, and when the queue is empty, it fires a change detection cycle . The runOutsideAngular() method runs code outside the zone, so the asynchronous tasks it schedules do not contribute to the queue and do not trigger change detection . The async/await syntax is a special case: it cannot be monkey-patched directly, so the Angular CLI downlevels it to a form that Zone.js can track . Zoneless change detection replaces Zone.js’s broad interception with a set of explicit notification APIs — signals, markForCheck(), setInput(), and bound listeners .


The Zone.js’s patching mechanism

Zone.js works by wrapping the native asynchronous APIs. Every call to a patched API runs inside a “zone,” and Zone.js tracks when the task starts and when it completes .

The patched APIs. The setTimeout, the setInterval, the Promise, the addEventListener, the XMLHttpRequest, the fetch, and the requestAnimationFrame are the common ones. Zone.js replaces each with a wrapped version that notifies the zone’s runtime .

Why the patching matters. The patching is the mechanism that lets Angular know when to run change detection. Without it, Angular would have no way to know that an asynchronous operation completed and the view might be stale.

Why the patching is the source of the overhead. The patching is the cost’s. Every patched API has the wrapper’s, and the wrapper’s is the overhead’s. The Zone.js is the payload’s (the 36-149 KB uncompressed, the 12-29 KB gzipped) .

Why the patching is the source of the incompatibility. The patching is the incompatibility’s, and the incompatibility’s is the library’s. Some libraries rely on the unpatched APIs, and the patched versions break them .

Why the patching is the source of the debugging’s difficulty. The patching is the stack trace’s, and the stack trace’s is the harder’s. The Zone.js makes the stack traces harder to understand, and the harder’s is the debugging’s cost .

Why the patching is the source of the async/await’s gotcha. The patching is the async/await’s, and the async/await’s is the special’s. The async/await cannot be monkey-patched directly, and the downleveling is the workaround .

Why the patching is the source of the zone pollution’s. The patching is the zone pollution’s, and the zone pollution’s is the unnecessary’s. The third-party libraries trigger the change detection’s, and the unnecessary’s is the performance’s cost .

Why the patching is the source of the zoneless’s motivation. The patching is the zoneless’s motivation’s, and the motivation’s is the removal’s. The zoneless removes the Zone.js, and the removal’s is the performance’s .

Why the patching is the historical’s. The patching is the historical’s, and the historical’s is the Angular’s. The Angular’s was the Zone.js’s, and the Zone.js’s was the patching’s. The two are the pair, and the pair is the design’s.


The NgZone and the ZoneImpl

The NgZone is Angular’s interface to Zone.js, and the ZoneImpl is the runtime that intercepts the asynchronous operations .

The NgZone. The NgZone is the service’s, and the service’s is the injectable’s. The NgZone provides the run(), the runOutsideAngular(), the onMicrotaskEmpty, the onStable, and the isStable .

Why the NgZone matters. The NgZone is the bridge’s, and the bridge’s is the Zone.js’s. The NgZone is the Angular’s, and the Angular’s is the change detection’s. The two are the pair, and the pair is the design’s.

The ZoneImpl. The ZoneImpl is the runtime’s, and the runtime’s is the Zone.js’s. The ZoneImpl intercepts the asynchronous operations, and the interception’s is the tracking’s .

Why the ZoneImpl matters. The ZoneImpl is the foundational’s, and the foundational’s is the Zone.js’s. The ZoneImpl is the runtime’s, and the runtime’s is the interception’s. The two are the pair, and the pair is the design’s.

The patching modules. The patching modules are the Zone.js‘s, and the Zone.js‘s is the APIs’s. The patching modules modify the native APIs, and the modification’s is the tracking’s .

Why the patching modules matter. The patching modules are the specific’s, and the specific’s is the API’s. The patching modules are the setTimeout‘s, the Promise‘s, the XHR‘s. The two are the pair, and the pair is the design’s.

Why the NgZone‘s onMicrotaskEmpty matters. The NgZone‘s onMicrotaskEmpty is the trigger’s, and the trigger’s is the change detection’s. The onMicrotaskEmpty emits when the microtask queue is empty, and the empty’s is the tick’s .

Why the NgZone‘s runOutsideAngular matters. The NgZone‘s runOutsideAngular is the outside’s, and the outside’s is the no-change-detection’s. The runOutsideAngular runs the code outside the zone, and the outside’s is the performance’s .


The onMicrotaskEmpty‘s trigger

The onMicrotaskEmpty is the event that fires when the microtask queue is empty, and it is the trigger for the change detection .

The microtask queue. The microtask queue is the Zone.js’s, and the Zone.js’s is the patched’s APIs. The patched APIs contribute to the microtask queue, and the queue’s is the tracking’s .

The onMicrotaskEmpty‘s emission. The onMicrotaskEmpty emits when the queue is empty, and the empty’s is the after’s. The Angular listens to the onMicrotaskEmpty, and the listen’s is the tick’s .

The ApplicationRef.tick()‘s call. The ApplicationRef.tick() is the change detection’s, and the change detection’s is the whole’s. The onMicrotaskEmpty‘s subscription calls the tick(), and the tick()‘s is the cycle’s .

this._onMicrotaskEmptySubscription = this.zone.onMicrotaskEmpty.subscribe({
  next: () => {
    if (this.changeDetectionScheduler?.runningTick) {
      return;
    }
    this.zone.run(() => {
      this.applicationRef.tick();
    });
  }
});

The onMicrotaskEmpty‘s subscription is the source’s, and the source’s is the Angular’s. The tick() is the change detection’s, and the change detection’s is the whole’s. The two are the pair, and the pair is the design’s .

Why the onMicrotaskEmpty matters. The onMicrotaskEmpty is the trigger’s, and the trigger’s is the change detection’s. The onMicrotaskEmpty is the Zone.js’s, and the Zone.js’s is the automatic’s. The two are the pair, and the pair is the design’s.

Why the onMicrotaskEmpty‘s emission matters. The onMicrotaskEmpty‘s emission is the after’s, and the after’s is the task’s. The onMicrotaskEmpty emits after the task’s, and the task’s is the patched’s API’s. The two are the pair, and the pair is the design’s.

Why the onMicrotaskEmpty‘s the empty’s matters. The onMicrotaskEmpty‘s the empty’s is the queue’s, and the queue’s is the microtask’s. The onMicrotaskEmpty is the empty’s, and the empty’s is the after’s. The two are the pair, and the pair is the design’s.

Why the onMicrotaskEmpty‘s the tick‘s matters. The onMicrotaskEmpty‘s the tick‘s is the change detection’s, and the change detection’s is the whole’s. The onMicrotaskEmpty‘s the tick‘s is the ApplicationRef‘s, and the ApplicationRef‘s is the root’s. The two are the pair, and the pair is the design’s.

Why the onMicrotaskEmpty matters for the zoneless’s. The onMicrotaskEmpty matters for the zoneless’s, and the zoneless’s is the removal’s. The onMicrotaskEmpty is the Zone.js’s, and the Zone.js’s is the removed’s. The two are the pair, and the pair is the design’s .


The runOutsideAngular and the zone pollution

The runOutsideAngular is the performance’s, and the performance’s is the zone pollution’s.

The runOutsideAngular. The runOutsideAngular is the outside’s, and the outside’s is the no-change-detection’s.

zone.runOutsideAngular(() => {
  setInterval(() => {
    // the no-change-detection's
  }, 1000);
});

The zone.runOutsideAngular(() => { ... }) is the outside’s, and the outside’s is the no-change-detection’s. The setInterval in the runOutsideAngular, and the runOutsideAngular‘s is the performance’s .

Why the runOutsideAngular matters. The runOutsideAngular is the performance’s, and the performance’s is the zone pollution’s. The runOutsideAngular prevents the change detection’s, and the change detection’s is the expensive’s. The two are the pair, and the pair is the design’s .

The zone pollution. The zone pollution is the unnecessary’s, and the unnecessary’s is the third-party’s. The third-party libraries trigger the change detection’s, and the trigger’s is the unnecessary’s .

Why the zone pollution matters. The zone pollution is the performance’s, and the performance’s is the unnecessary’s. The zone pollution is the setInterval‘s, the requestAnimationFrame‘s, the third-party’s. The two are the pair, and the pair is the design’s .

The run‘s re-entry. The run‘s re-entry is the inside’s, and the inside’s is the change detection’s.

zone.run(() => {
  this.count++;  // the change detection's
});

The zone.run(() => { ... }) is the inside’s, and the inside’s is the change detection’s. The run‘s re-entry is the trigger’s, and the trigger’s is the change detection’s. The two are the pair, and the pair is the design’s .

Why the run‘s re-entry matters. The run‘s re-entry is the inside’s, and the inside’s is the change detection’s. The run‘s re-entry is the after’s, and the after’s is the runOutsideAngular‘s. The two are the pair, and the pair is the design’s .

Why the runOutsideAngular‘s the event’s matters. The runOutsideAngular‘s the event’s is the listener’s, and the listener’s is the outside’s. The addEventListener in the runOutsideAngular, and the runOutsideAngular‘s is the no-change-detection’s. The two are the pair, and the pair is the design’s .

Why the runOutsideAngular‘s the third-party’s matters. The runOutsideAngular‘s the third-party’s is the chart’s, and the chart’s is the outside’s. The runOutsideAngular for the chart’s animation, and the animation’s is the no-change-detection’s. The two are the pair, and the pair is the design’s .


The async/await‘s gotcha

The async/await is the special’s, and the special’s is the Zone.js’s.

The async/await‘s the patching’s. The async/await‘s the patching’s is the impossible’s, and the impossible’s is the monkey’s. The async/await cannot be monkey-patched directly, and the directly’s is the Zone.js’s .

Why the async/await matters. The async/await is the special’s, and the special’s is the Zone.js’s. The async/await is the syntax’s, and the syntax’s is the impossible’s. The two are the pair, and the pair is the design’s .

The downleveling’s workaround. The downleveling’s workaround is the CLI’s, and the CLI’s is the async/await‘s.

// The async/await's the downleveling's
async function foo() {
  await Promise.resolve();
  // the after's
}

The async function foo() { await Promise.resolve(); } is the async/await‘s, and the async/await‘s is the downleveling’s. The await Promise.resolve() is the after’s, and the after’s is the zone’s. The two are the pair, and the pair is the design’s .

Why the downleveling matters. The downleveling is the CLI’s, and the CLI’s is the async/await‘s. The downleveling transforms the async/await‘s, and the transform’s is the Zone.js’s. The two are the pair, and the pair is the design’s .

Why the async/await‘s the gotcha’s matters. The async/await‘s the gotcha’s is the after’s, and the after’s is the zone’s. The async/await‘s the after’s is the outside’s, and the outside’s is the no-change-detection’s. The two are the pair, and the pair is the design’s .

Why the async/await‘s the NgZone‘s matters. The async/await‘s the NgZone‘s is the isInAngularZone‘s, and the isInAngularZone‘s is the false’s. The async/await‘s the NgZone‘s is the false’s, and the false’s is the outside’s. The two are the pair, and the pair is the design’s .

Why the async/await‘s the zoneless’s matters. The async/await‘s the zoneless’s is the no-issue’s, and the no-issue’s is the removal’s. The zoneless removes the Zone.js, and the removal’s is the async/await‘s the no-issue’s. The two are the pair, and the pair is the design’s .


The zoneless’s replacement

The zoneless’s replacement is the explicit’s, and the explicit’s is the notification’s.

The zoneless’s notifications. The zoneless’s notifications are the markForCheck‘s, the setInput‘s, the signal’s, the bound’s listeners, the attach’s, the remove’s .

Why the zoneless’s notifications matter. The zoneless’s notifications are the explicit’s, and the explicit’s is the precise’s. The zoneless’s notifications are the Angular’s, and the Angular’s is the APIs’s. The two are the pair, and the pair is the design’s .

The signals’s role. The signals’s role is the automatic’s, and the automatic’s is the notification’s. The signals notify the consumers’s, and the consumers’s is the mark’s. The two are the pair, and the pair is the design’s .

Why the signals’s role matters. The signals’s role is the modern’s, and the modern’s is the reactive’s. The signals’s role is the precise’s, and the precise’s is the fine-grained’s. The two are the pair, and the pair is the design’s .

The markForCheck‘s role. The markForCheck‘s role is the manual’s, and the manual’s is the explicit’s. The markForCheck is the notification’s, and the notification’s is the zoneless’s. The two are the pair, and the pair is the design’s .

Why the markForCheck‘s role matters. The markForCheck‘s role is the escape’s, and the escape’s is the manual’s. The markForCheck‘s role is the specific’s, and the specific’s is the design’s. The two are the pair, and the pair is the design’s .

The zoneless’s default’s. The zoneless’s default’s is the Angular 21’s, and the Angular 21’s is the default’s. The zoneless’s default’s is the modern’s, and the modern’s is the removal’s. The two are the pair, and the pair is the design’s .

Why the zoneless’s default’s matters. The zoneless’s default’s is the modern’s, and the modern’s is the Angular 21’s. The zoneless’s default’s is the standard’s, and the standard’s is the future’s. The two are the pair, and the pair is the design’s .

Why the zoneless’s default’s the Zone.js’s matters. The zoneless’s default’s the Zone.js’s is the removal’s, and the removal’s is the bundle’s. The zoneless’s default’s the Zone.js’s is the 36-149 KB’s, and the 36-149 KB’s is the savings’s. The two are the pair, and the pair is the design’s .


Complete Example Session

// ============================================
// PART 1: THE ZONE.JS'S PATCHING
// ============================================

// Zone.js patches the native APIs:
// - setTimeout, setInterval
// - Promise
// - addEventListener
// - XMLHttpRequest, fetch
// - requestAnimationFrame

// ============================================
// PART 2: THE NgZone'S onMicrotaskEmpty
// ============================================

this._onMicrotaskEmptySubscription = this.zone.onMicrotaskEmpty.subscribe({
  next: () => {
    if (this.changeDetectionScheduler?.runningTick) {
      return;
    }
    this.zone.run(() => {
      this.applicationRef.tick();
    });
  }
});

// ============================================
// PART 3: THE runOutsideAngular
// ============================================

zone.runOutsideAngular(() => {
  setInterval(() => {
    // the no-change-detection's
  }, 1000);
});

// ============================================
// PART 4: THE run's re-entry
// ============================================

zone.runOutsideAngular(() => {
  setInterval(() => {
    zone.run(() => {
      this.count++;  // the change detection's
    });
  }, 1000);
});

// ============================================
// PART 5: THE ZONE POLLUTION
// ============================================

// The third-party library:
Plotly.newPlot('chart', data);  // the unnecessary's

// The fix:
zone.runOutsideAngular(() => {
  Plotly.newPlot('chart', data);
});

// ============================================
// PART 6: THE ASYNC/AWAIT'S GOTCHA
// ============================================

async function foo() {
  console.log('Inside Angular zone?', NgZone.isInAngularZone());  // true
  await Promise.resolve();
  console.log('Inside Angular zone?', NgZone.isInAngularZone());  // false
}

// The CLI downlevels the async/await, so the production's is the patched's.

// ============================================
// PART 7: THE ZONELESS'S NOTIFICATIONS
// ============================================

// The zoneless's notifications:
// - ChangeDetectorRef.markForCheck
// - ComponentRef.setInput
// - Updating a signal read in a template
// - Bound host or template listeners
// - Attaching a view marked dirty

// ============================================
// PART 8: THE SIGNALS'S ROLE
// ============================================

count = signal(0);

increment(): void {
  this.count.update((v) => v + 1);  // the automatic's
}

// ============================================
// PART 9: THE ZONELESS'S DEFAULT
// ============================================

// Angular 21+:
// The zoneless is the default's.
// The provideZonelessChangeDetection() is the explicit's.

// ============================================
// PART 10: WHAT NOT TO DO
// ============================================

// Don't forget the markForCheck for the async's
setTimeout(() => { this.value = 'x'; });  // the no-trigger's // ⚠️

// Don't use the runOutsideAngular for the state's
zone.runOutsideAngular(() => { this.count++; });  // the no-trigger's // ⚠️

// Don't forget the zone's re-entry
zone.runOutsideAngular(() => {
  setInterval(() => { this.count++; });  // the no-trigger's // ⚠️
});

// Don't rely on the async/await's the zone's
async function foo() {
  await Promise.resolve();
  this.value = 'x';  // the outside's // ⚠️
}

// Don't use the Zone.js's APIs in the zoneless's
NgZone.onMicrotaskEmpty  // the never's // ⚠️

The ten parts cover the Zone.js’s patching, the NgZone‘s onMicrotaskEmpty, the runOutsideAngular, the run‘s re-entry, the zone pollution, the async/await‘s gotcha, the zoneless’s notifications, the signals’s role, the zoneless’s default, and the anti-patterns.


Quick Reference

The Zone.js’s Patched APIs

APIPurpose
The setTimeoutThe timer’s
The setIntervalThe timer’s
The PromiseThe async’s
The addEventListenerThe event’s
The XMLHttpRequestThe HTTP’s
The fetchThe HTTP’s
The requestAnimationFrameThe animation’s

The NgZone‘s Methods

MethodPurpose
The run()The inside’s
The runOutsideAngular()The outside’s
The onMicrotaskEmptyThe trigger’s
The onStableThe stable’s
The isStableThe stable’s
The isInAngularZone()The check’s

The Zoneless’s Notifications

NotificationAPI
The markForCheckThe ChangeDetectorRef
The setInputThe ComponentRef
The signal’s updateThe signal.set()
The bound’s listenerThe (click)
The attach’sThe ViewContainerRef

The Zone.js’s Overhead

ItemValue
The payload’sThe 36-149 KB uncompressed
The gzipped’sThe 12-29 KB
The startup’sThe overhead’s
The debugging’sThe harder’s

The Migration’s Phases

PhasePurpose
The config’sThe provideZonelessChangeDetection()
The component’sThe OnPush + the signals
The test’sThe provideZonelessChangeDetection()
The polyfill’sThe zone.js‘s removal

Best Practices

✅ Do This:

// Use the runOutsideAngular for the third-party's
zone.runOutsideAngular(() => Plotly.newPlot('chart', data));   // ✅
// Use the run's re-entry for the state's
zone.runOutsideAngular(() => {
  setInterval(() => zone.run(() => this.count++), 1000);
});                                                            // ✅
// Use the signals for the zoneless's
count = signal(0);                                             // ✅
// Use the markForCheck for the async's
this.cdr.markForCheck();                                       // ✅
// Use the provideZonelessChangeDetection for the new's
provideZonelessChangeDetection();                               // ✅
// Use the OnPush for the zoneless's
changeDetection: ChangeDetectionStrategy.OnPush;                // ✅

❌ Don’t Do This:

// Don't forget the markForCheck for the async's
setTimeout(() => { this.value = 'x'; });  // the no-trigger's  // ⚠️
// Don't use the runOutsideAngular for the state's
zone.runOutsideAngular(() => { this.count++; });  // the no-trigger's // ⚠️
// Don't forget the zone's re-entry
zone.runOutsideAngular(() => {
  setInterval(() => { this.count++; });  // the no-trigger's   // ⚠️
});
// Don't rely on the async/await's the zone's
async function foo() {
  await Promise.resolve();
  this.value = 'x';  // the outside's                          // ⚠️
}
// Don't use the Zone.js's APIs in the zoneless's
NgZone.onMicrotaskEmpty  // the never's                          // ⚠️

Common Pitfalls

PitfallProblemSolution
The missing markForCheckThe stale’sThe markForCheck()
The runOutsideAngular‘s state’sThe no-trigger’sThe run()
The async/await’s the outside’sThe no-trigger’sThe signal’s
The zone pollution’sThe unnecessary’sThe runOutsideAngular
The onMicrotaskEmpty‘s the zoneless’sThe never’sThe removal’s
The async/await‘s the zone’sThe outside’sThe downleveling’s
The third-party’s the zone’sThe pollution’sThe runOutsideAngular

Real-World Examples

1. The Zone.js’s patching

// Zone.js patches the setTimeout, the Promise, the addEventListener.

2. The onMicrotaskEmpty

this.zone.onMicrotaskEmpty.subscribe(() => this.applicationRef.tick());

3. The runOutsideAngular

zone.runOutsideAngular(() => setInterval(...));

4. The run‘s re-entry

zone.run(() => this.count++);

5. The zone pollution

zone.runOutsideAngular(() => Plotly.newPlot('chart', data));

6. The async/await‘s gotcha

console.log(NgZone.isInAngularZone());  // true
await Promise.resolve();
console.log(NgZone.isInAngularZone());  // false

7. The zoneless’s notifications

// The markForCheck, the setInput, the signal's, the bound's listeners.

8. The signals’s role

count = signal(0);

9. The zoneless’s default

// Angular 21+: the zoneless is the default's.

10. The provideZonelessChangeDetection

provideZonelessChangeDetection();

Visual: The Zone.js’s Patching

┌──────────────────────────────────────────────┐
│  THE NATIVE APIs                             │
│    setTimeout, Promise, addEventListener     │
│    XHR, fetch, requestAnimationFrame         │
│                                              │
│         │  Zone.js patches                   │
│         ▼                                    │
│  THE PATCHED APIs                            │
│    The wrapper's, the zone's tracking's      │
│                                              │
│         │  The microtask queue               │
│         ▼                                    │
│  THE NgZone's onMicrotaskEmpty               │
│    The queue is empty's                      │
│                                              │
│         │  The trigger's                     │
│         ▼                                    │
│  THE ApplicationRef.tick()                   │
│    The change detection's                    │
│                                              │
└──────────────────────────────────────────────┘

Visual: The onMicrotaskEmpty’s Trigger

┌──────────────────────────────────────────────┐
│  The patched API's call (setTimeout)         │
│    │                                         │
│    ▼                                         │
│  The microtask queue's add                   │
│    │                                         │
│    ▼                                         │
│  The task's execution                        │
│    │                                         │
│    ▼                                         │
│  The microtask queue's empty                 │
│    │                                         │
│    ▼                                         │
│  The onMicrotaskEmpty's emit                 │
│    │                                         │
│    ▼                                         │
│  The ApplicationRef.tick()                   │
│    │                                         │
│    ▼                                         │
│  The change detection's                      │
│                                              │
└──────────────────────────────────────────────┘

Visual: The runOutsideAngular’s

┌──────────────────────────────────────────────┐
│  THE INSIDE'S (the zone's)                   │
│    setInterval → the change detection's      │
│                                              │
├──────────────────────────────────────────────┤
│  THE OUTSIDE'S (the runOutsideAngular's)     │
│    setInterval → the no-change-detection's   │
│                                              │
│  THE RE-ENTRY'S                             │
│    zone.run(() => { ... }) → the inside's    │
│                                              │
└──────────────────────────────────────────────┘

Visual: The async/await’s Gotcha

┌──────────────────────────────────────────────┐
│  console.log(NgZone.isInAngularZone())       │
│    → true (the inside's)                     │
│                                              │
│  await Promise.resolve()                     │
│    → the outside's (the zone's loss's)       │
│                                              │
│  console.log(NgZone.isInAngularZone())       │
│    → false (the outside's)                   │
│                                              │
│  The CLI's downleveling's is the fix's.      │
│                                              │
└──────────────────────────────────────────────┘

Visual: The Zoneless’s Notifications

┌──────────────────────────────────────────────┐
│  THE ZONELESS'S NOTIFICATIONS:               │
│                                              │
│  ✅ ChangeDetectorRef.markForCheck           │
│  ✅ ComponentRef.setInput                    │
│  ✅ The signal's update                      │
│  ✅ The bound's listeners                    │
│  ✅ The attach's of the dirty's              │
│                                              │
│  ❌ The plain property's mutation            │
│  ❌ The setInterval's without the signal's   │
│  ❌ The setTimeout's without the markForCheck│
│                                              │
└──────────────────────────────────────────────┘

Summary

ItemValue
The Zone.jsThe patching’s
The patched APIsThe setTimeout, the Promise, the addEventListener
The NgZoneThe bridge’s
The ZoneImplThe runtime’s
The onMicrotaskEmptyThe trigger’s
The ApplicationRef.tick()The change detection’s
The runOutsideAngularThe outside’s
The runThe inside’s
The async/awaitThe gotcha’s
The zonelessThe explicit’s

Key takeaways:

  • The Zone.js works by monkey-patching the native asynchronous APIs — the setTimeout, the setInterval, the Promise, the addEventListener, the XMLHttpRequest, the fetch, the requestAnimationFrame
  • The NgZone is Angular’s interface to Zone.js, and the ZoneImpl is the runtime — the NgZone provides the run(), the runOutsideAngular(), and the onMicrotaskEmpty
  • The onMicrotaskEmpty is the trigger for the change detection — the Angular listens to the microtask queue’s empty event and calls the ApplicationRef.tick()
  • The runOutsideAngular runs code outside the zone, preventing the change detection — it is the performance’s tool for the frequent’s and the third-party’s operations
  • The run re-enters the zone, triggering the change detection — it is the pattern’s for the state’s changes in the outside’s callbacks
  • The async/await is the special’s gotcha — it cannot be monkey-patched directly, so the Angular CLI downlevels it to a form that Zone.js can track
  • The zone pollution is the unnecessary’s change detection — the third-party libraries trigger the cycles, and the runOutsideAngular is the fix
  • The zoneless replaces Zone.js’s broad interception with explicit notifications — the markForCheck, the setInput, the signals, the bound’s listeners, the attach’s
  • The signals are the modern’s reactive foundation — they provide the precise’s notifications, and they are the recommended’s for the zoneless’s
  • The zoneless is the default in Angular 21+ — the Zone.js’s removal saves the 36-149 KB, and the migration’s phases are the config’s, the component’s, the test’s, and the polyfill’s

Remember: The Zone.js is the machinery’s, and the NgZone is the bridge’s. The onMicrotaskEmpty is the trigger’s, and the runOutsideAngular is the performance’s. The async/await is the gotcha’s, and the zoneless is the future’s. The signals are the modern’s, and the markForCheck is the escape’s. The internals’s is the understanding’s, and the understanding’s is the skill’s.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!