Skip to the solution
HardFrontend9 min read

What is Zone.js, how does it monkey-patch browser APIs, and how do you build a Zoneless Angular application?

814 views

The problem

Question presented to candidate: "Explain what Zone.js actually does to the browser when it loads, how Angular used it to know when to update the screen, and then walk me through taking an existing Zone-based app to zoneless."

What a strong answer should cover:

  • Monkey-patching: Zone.js replaces async APIs (setTimeout, setInterval, Promise, addEventListener, XHR, fetch, requestAnimationFrame) with wrappers that turn each call into a task and keep the original under a __zone_symbol__ key.
  • A zone is an execution context that survives async boundaries; Zone.current.fork({...}) adds hooks like onScheduleTask, onInvokeTask, onHasTask. Task kinds: microTask, macroTask, eventTask.
  • Angular's NgZone is one such fork; when it settles, Zone-based apps run change detection.
  • Limits: native async/await cannot be patched, so Zone apps need the build to downlevel it; patching costs bundle size and makes stack traces noisier.
  • Zoneless migration: remove provideZoneChangeDetection(), remove zone.js from polyfills (build and test), move template state to signals or markForCheck(), replace onStable/onMicrotaskEmpty logic, use PendingTasks for SSR stability. Zoneless is the default since v21.

Clarifying questions expected:

  • "Which Angular version is the app on, and does it use OnPush already?"
  • "Do any third-party libraries rely on Zone.js-triggered change detection?"

Code / implementation expected: Optional β€” a forked zone logging its hooks, and the config diff for going zoneless.

  • performance
  • zoneless

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

9 min read Β· 8 sections

Read the code

AngularSee the patch: forked zone hooks around setTimeout and Promise.then
import { Component, signal } from "@angular/core";

// zone.js puts a global Zone object on the page when it is loaded.
declare const Zone: any;

@Component({
  selector: "app-root",
  template: `
    <button (click)="run()">fork a zone and schedule work</button>
    <ol>@for (line of log(); track $index) { <li>{{ line }}</li> }</ol>
  `,
})
export class AppComponent {
  log = signal<string[]>([]);

  run() {
    const lines: string[] = [];
    if (typeof Zone === "undefined") {
      this.log.set(["zone.js is not loaded on this page: nothing is patched"]);
      return;
    }
    const g = globalThis as any;
    // 1. The patch: the global setTimeout is a wrapper, the browser original is kept aside.
    lines.push(`setTimeout is the original: ${g.setTimeout === g.__zone_symbol__setTimeout}`);
    lines.push(`wrapper remembers original: ${g.setTimeout.__zone_symbol__OriginalDelegate === g.__zone_symbol__setTimeout}`);
    lines.push(`Promise is now: ${Promise.name}`);

    // 2. A zone is an execution context with hooks around every async task.
    const demo = Zone.current.fork({
      name: "demo",
      onScheduleTask: (d: any, _c: any, t: any, task: any) => {
        lines.push(`schedule ${task.type} (${task.source})`);
        return d.scheduleTask(t, task);
      },
      onInvokeTask: (d: any, _c: any, t: any, task: any, self: any, args: any) => {
        lines.push(`invoke ${task.type} (${task.source})`);
        return d.invokeTask(t, task, self, args);
      },
      onHasTask: (d: any, _c: any, t: any, state: any) => {
        lines.push(`hasTask macro=${state.macroTask} micro=${state.microTask}`);
        return d.hasTask(t, state);
      },
    });

    demo.run(() => {
      setTimeout(() => lines.push(`timer callback ran in zone "${Zone.current.name}"`));
      Promise.resolve().then(() => lines.push(`then callback ran in zone "${Zone.current.name}"`));
    });

    // Read the log once both tasks are done (this timer is scheduled outside the demo zone).
    setTimeout(() => this.log.set([...lines, `back outside, Zone.current is "${Zone.current.name}"`]), 20);
  }
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 93 of 121 in Angular

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Angular questions
Up nextHard

What is model() β€” two-way signal binding with model inputs?

Continue