Skip to the solution
HardFrontend9 min read

What is zoneless Angular and how does it improve performance in Angular 18?

731 views

The problem

Question presented to candidate: "Angular 18 announced zoneless change detection. What does zoneless mean, what replaces Zone.js as the trigger for rendering, and where do the performance gains actually come from? Is the Angular 18 story still accurate today?"

What a strong answer should cover:

  • Zoneless means Angular does not use Zone.js to decide when to run change detection; a scheduler runs only after explicit notifications: signal changes read by templates, template/host events, markForCheck() (and so AsyncPipe), setInput, views attached or removed.
  • Timeline: experimental in v18 (May 2024, provideExperimentalZonelessChangeDetection), developer preview in v20.0, stable in v20.2 (provideZonelessChangeDetection), default in v21; in v22 Zone.js is opt-in with provideZoneChangeDetection().
  • Gains: no zone.js polyfill in the bundle, no change detection run per async task, native async/await (no downlevelling), cleaner stack traces; biggest gains when combined with OnPush and signals.
  • Costs: code that mutated plain fields in callbacks goes stale; NgZone.onStable logic stops firing; SSR needs PendingTasks for work Angular cannot see.
  • Can quote real numbers rather than "it is faster".

Clarifying questions expected:

  • "Are we on an Angular version where zoneless is the default (v21+) or still opting in?"
  • "Is state in signals already, or mostly plain fields and subscriptions?"

Code / implementation expected: Optional β€” a component showing which kinds of updates render without Zone.js.

  • zoneless
  • performance
  • angular-18

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 Β· 9 sections

Read the code

AngularZoneless-ready state: signal, async pipe, and a plain field that needs markForCheck
import { AsyncPipe } from "@angular/common";
import { ChangeDetectorRef, Component, NgZone, inject, signal } from "@angular/core";
import { map, take, timer } from "rxjs";

@Component({
  selector: "app-root",
  imports: [AsyncPipe],
  template: `
    <p id="zone">NgZone in this app: {{ zoneKind }}</p>
    <p id="sig">signal set in setTimeout: {{ fromSignal() }}</p>
    <p id="pipe">observable via async pipe: {{ ticks$ | async }}</p>
    <p id="plain">plain field set in setTimeout: {{ plain }}</p>
    <button (click)="refresh()">markForCheck()</button>
  `,
})
export class AppComponent {
  private cdr = inject(ChangeDetectorRef);
  // Zoneless is the default since v21: Angular injects a no-op NgZone.
  zoneKind = NgZone.isInAngularZone() ? "real NgZone (zone-based)" : "no-op NgZone (zoneless)";

  fromSignal = signal("waiting");
  plain = "waiting";
  // AsyncPipe calls markForCheck() on every emission, so it works without zone.js.
  ticks$ = timer(0, 10).pipe(take(3), map(n => `emission ${n + 1} of 3`));

  constructor() {
    // Spaced out so each write is observed on its own.
    setTimeout(() => this.fromSignal.set("updated"), 50); // notifies the scheduler: re-rendered
    setTimeout(() => (this.plain = "updated"), 100);      // notifies nobody: the DOM keeps "waiting"
  }

  // Any notification (markForCheck, a template event, a signal) lets the plain field catch up.
  refresh() { this.cdr.markForCheck(); }
}

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 95 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 Angular Universal / SSR?

Continue