Skip to content
Angular interview 5 min read

Change Detection Questions

Change detection is one of the deepest topics interviewers use to separate developers who use Angular from those who understand it. The questions below move from the mechanics of how Angular re-renders a view, through the OnPush optimization strategy, the role of Zone.js, and finally the modern zoneless architecture built on signals. Knowing why each piece exists matters more than reciting definitions.

What is change detection and how does Angular run it?

Change detection is the process by which Angular synchronizes the component’s data model with the DOM. After any potentially state-changing event, Angular walks the component tree from the root down, re-evaluating each binding expression and updating the DOM where a value has changed.

Internally, Angular compiles every template into a detectChanges-style function. When a check runs, it compares the current value of each bound expression against the previously rendered value. The tree is traversed top-down, in a single pass, which is why Angular’s strategy is sometimes called unidirectional data flow.

The trigger for a check is an asynchronous event: a DOM event, an HTTP response, a setTimeout, or a promise resolving. Traditionally, Zone.js intercepts these and tells Angular to run a check.

How does the default strategy differ from OnPush?

There are two strategies, set per component via the changeDetection metadata field.

AspectDefaultOnPush
When the component is checkedOn every CD cycle anywhere in the appOnly when triggered (see below)
CostHigher — checked frequentlyLower — skipped unless dirty
Required disciplineNoneImmutable inputs / signals / async pipe

With Default, a component is checked on every cycle regardless of whether its data changed. With OnPush, Angular skips the component (and its subtree) unless one of these happens:

  • An @Input() reference changes (new object identity, not a mutation).
  • An event fires from within the component’s template.
  • An observable bound with the async pipe emits.
  • A signal read in the template changes.
  • You call markForCheck() manually.
import { ChangeDetectionStrategy, Component, input } from '@angular/core';

@Component({
  selector: 'app-price-tag',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `<span class="tag">{{ amount() | currency }}</span>`,
})
export class PriceTagComponent {
  amount = input.required<number>();
}

Gotcha: under OnPush, mutating an object in place (this.user.name = 'x') will not trigger a check because the input reference is unchanged. Replace the reference instead (this.user = { ...this.user, name: 'x' }), or use signals.

What is markForCheck versus detectChanges?

Both live on ChangeDetectorRef, but they do opposite things.

  • markForCheck() flags the component and all its ancestors as dirty so they will be checked on the next cycle. It does not run detection immediately. This is the right tool for OnPush components updating from an async source you control.
  • detectChanges() runs change detection synchronously on this component and its children right now. Use it sparingly — usually only outside the Angular zone.
import { ChangeDetectorRef, Component, inject } from '@angular/core';

@Component({ /* ... OnPush ... */ })
export class FeedComponent {
  private cdr = inject(ChangeDetectorRef);
  items: string[] = [];

  loadFromWebSocket(socket: WebSocket) {
    socket.onmessage = (e) => {
      this.items = [...this.items, e.data];
      this.cdr.markForCheck(); // schedule a check
    };
  }
}

What role does Zone.js play?

Zone.js monkey-patches the browser’s asynchronous APIs — addEventListener, setTimeout, setInterval, Promise, and XMLHttpRequest. When any of these completes, the patched version notifies Angular’s NgZone, which then schedules a top-down change detection pass over the whole tree.

This is what makes Angular feel “magic”: you mutate a field in a click handler and the view updates with no extra code. The cost is that Zone.js runs detection on every such event, even ones that change nothing.

You can opt out for hot paths with NgZone.runOutsideAngular, which executes work without triggering detection:

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

@Component({ /* ... */ })
export class TickerComponent {
  private zone = inject(NgZone);

  start() {
    this.zone.runOutsideAngular(() => {
      setInterval(() => this.draw(), 16); // 60fps, no CD churn
    });
  }

  private draw() { /* canvas drawing, no bindings touched */ }
}

What is zoneless Angular?

Zoneless Angular (stable from v19) removes the Zone.js dependency entirely. Instead of intercepting every async API, the framework schedules change detection only when a signal read in a template changes, when an event handler runs, or when markForCheck() is called.

You enable it during bootstrap:

import { bootstrapApplication } from '@angular/platform-browser';
import { provideZonelessChangeDetection } from '@angular/core';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, {
  providers: [provideZonelessChangeDetection()],
});

Output:

✔ Application bootstrapped without Zone.js
  bundle size reduced (~13kB Zone.js polyfill removed)

The benefits are a smaller bundle, faster startup, and far fewer wasted detection cycles. The trade-off is that code relying on Zone.js auto-detection (mutating plain fields and expecting updates) must migrate to signals or explicit markForCheck() calls. In practice, signal-based components are already zoneless-ready.

How do signals change the picture?

Signals give Angular fine-grained reactivity. When a signal used in a template changes, Angular knows exactly which view depends on it and marks only that component dirty — no tree walk to find what changed. This is why signals are the foundation of zoneless: they make Zone.js’s broad interception unnecessary.

import { Component, signal, computed } from '@angular/core';

@Component({
  selector: 'app-counter',
  template: `
    <button (click)="count.set(count() + 1)">+</button>
    <p>Count: {{ count() }} · Doubled: {{ doubled() }}</p>
  `,
})
export class CounterComponent {
  count = signal(0);
  doubled = computed(() => this.count() * 2);
}

Best Practices

  • Default to OnPush for new components; treat Default change detection as the exception, not the rule.
  • Prefer signals and the async pipe over manual markForCheck() so dirtiness is tracked for you.
  • Keep @Input() data immutable — pass new references rather than mutating in place.
  • Move animation loops, canvas work, and high-frequency timers outside the Angular zone with runOutsideAngular.
  • Reach for detectChanges() only when you have intentionally left the zone and need a synchronous update.
  • Adopt zoneless on greenfield projects to cut bundle size and eliminate wasted cycles, migrating field mutations to signals first.
Last updated June 14, 2026
Was this helpful?