20 Angular Change Detection Interview Questions (Angular 22)

·13 min read
By ·Updated
angularinterview-questionschange-detectionperformancefrontendangular-22

Angular's change-detection model changed materially across versions 21 and 22. Zoneless scheduling is now the default, OnPush is the default component strategy, and ChangeDetectionStrategy.Default is a deprecated alias for Eager. Interview answers based only on “Zone.js checks the whole tree after every async event” describe an older configuration.

This guide contains exactly 20 questions and separates three ideas that are often conflated: what schedules change detection, which views are eligible, and which DOM bindings actually change.

Table of Contents

  1. Fundamentals
  2. OnPush and Eager
  3. Zoneless and Zone.js
  4. ChangeDetectorRef
  5. Rendering performance
  6. Debugging and testing
  7. Frequently asked questions

Fundamentals

1. What is Angular change detection?

Change detection synchronizes an Angular view with application state. During a pass, Angular evaluates template bindings and lifecycle work for eligible views, compares results with the values it previously rendered, and performs the necessary DOM updates.

Do not equate a component check with recreating its DOM. A check may evaluate bindings and write nothing. Likewise, browser layout and paint happen after JavaScript and have separate costs that Angular's traversal model alone does not explain.

2. What changed for change detection in Angular 22?

OnPush became the default strategy in Angular 22. The former Default strategy is now Eager; ChangeDetectionStrategy.Default remains a deprecated alias for compatibility.

Zoneless scheduling had already become the default in Angular 21. Therefore a current answer should not say that every Angular app ships Zone.js or that Default checks every component after every timer. Version, configured provider, view strategy and notification path all matter.

3. What are scheduling, traversal and DOM update?

They are distinct stages:

  1. Notification/scheduling: Angular learns that rendered state may need work.
  2. Traversal: Angular walks relevant attached views, skipping clean OnPush subtrees where allowed.
  3. Evaluation/update: bindings and hooks run; changed binding values lead to DOM writes.
  4. Browser rendering: style, layout, paint and compositing occur as the browser requires.

Optimizing the wrong stage produces cargo-cult fixes. A slow interaction may come from too many schedules, expensive template work, a third-party script, layout thrashing or network work—not simply “too many components.”

4. Which notifications schedule zoneless change detection?

Angular documents these notification paths:

  • ChangeDetectorRef.markForCheck()—also called by AsyncPipe;
  • ComponentRef.setInput();
  • updating a signal read by a template;
  • a bound host or template listener;
  • attaching a dirty view;
  • removing a view;
  • registering a render hook, when the hook also performs one of the relevant notifications.

A Promise, timer or arbitrary library callback is not enough by itself in zoneless mode. The state change must reach Angular through a supported path.


OnPush and Eager

5. How do OnPush and Eager differ?

When traversal reaches an Eager view, Angular checks it. OnPush lets Angular skip a clean subtree until an input/event/reactive/manual notification makes it eligible. Since v22, OnPush is the default.

Eager can be appropriate for compatibility boundaries that cannot reliably notify Angular, especially a library host creating unknown user components. It is not a substitute for understanding state ownership, and OnPush is not a guarantee of good performance.

6. When is an OnPush component checked?

Important paths include:

  • a template binding supplies a changed input;
  • Angular handles an event in the component or a descendant;
  • markForCheck() or a wrapper such as AsyncPipe marks it;
  • a signal read in its template changes;
  • ComponentRef.setInput() sets an input;
  • an explicit local detectChanges() checks it.

An event in one branch may cause ancestors to be checked while unrelated clean OnPush branches are skipped. “Only the clicked component renders” is too simplistic.

7. How does Angular compare OnPush inputs?

The current subtree-skipping guide describes the template-bound input comparison with loose equality (==). Signals separately use Object.is() by default and may define a custom equality function. Do not merge these into the common but inaccurate claim that all Angular reactivity uses ===.

More importantly, use stable, intentional identity at component boundaries. The equality detail is not a reason to rely on coercion between differently typed inputs; keep inputs typed and normalized.

8. Why can mutating an input leave a child stale?

If a parent mutates an object already passed to an OnPush child, the input identity supplied by the binding has not changed. If no event, signal or manual notification makes that child eligible, its view may be skipped.

type User = Readonly<{ id: string; name: string }>;
 
user: User = { id: 'u1', name: 'Ada' };
 
rename() {
  this.user = { ...this.user, name: 'Grace' };
}

Replacing a value is a useful ownership convention, but Angular does not enforce deep immutability. Also, a mutation executed by the child's own bound click handler may appear because the event marks that subtree. The bug is an implicit notification/ownership contract, not mutation as a metaphysical rule.

9. How do Signals interact with OnPush?

When a template reads a signal, Angular records that view as a dependency. When the signal publishes a non-equal value, Angular marks the view so it is refreshed during the next scheduled pass.

readonly count = signal(0);
readonly doubled = computed(() => this.count() * 2);
 
increment() {
  this.count.update((value) => value + 1);
}

Signals use Object.is() by default. Mutating an object held by a signal without calling set()/update() does not publish a value. A custom deep-equality function can also suppress an update even when a new reference is supplied; use it only after measuring its computation and semantics.

10. How does AsyncPipe work with OnPush?

AsyncPipe subscribes to an Observable or Promise, exposes the latest value, marks its view for check when a new value arrives, and unsubscribes when the view is destroyed or the source reference changes.

It is usually safer than a manual component subscription for presentation data, but it does not cancel server side effects merely because a client subscription ends. It also does not make an expensive high-frequency stream free; use appropriate rate control and state ownership.


Zoneless and Zone.js

11. What was Zone.js's role, and is it still required?

Zone.js patches many asynchronous platform APIs and lets zone-based Angular schedule change detection when tracked work completes. It reduced the need for explicit notifications but could cause unnecessary cycles, patching incompatibilities and difficult stack traces.

It is not required in Angular 21+. Zoneless is stable and default. Existing applications may temporarily opt back into zone-based scheduling with provideZoneChangeDetection() and the Zone.js polyfill while dependencies are migrated.

12. Is NgZone useful in a zoneless application?

Migration-era code using NgZone.onMicrotaskEmpty, onUnstable, onStable or isStable must be audited: those observables do not behave as zone-based code expects in zoneless mode, and isStable remains true.

runOutsideAngular() is a zone-based optimization for avoiding zone-triggered cycles. In a truly zoneless app, arbitrary async activity already does not schedule a check, so adding runOutsideAngular() is usually not the lever. Focus on supported notifications and actual profile evidence.


ChangeDetectorRef

13. What is the difference between markForCheck and detectChanges?

markForCheck() marks a view dirty so Angular includes it in a scheduled traversal; in zoneless mode it is also a notification that schedules work. It supports batching with other changes.

detectChanges() synchronously checks that view and its children locally. It does not mean “wait until the browser paints,” and it is not the routine solution for every WebSocket or third-party callback. Prefer a signal, AsyncPipe, setInput() or markForCheck() when the normal scheduler should own the update.

14. When should you detach and reattach a view?

detach() removes a view from the normal change-detection tree. Even a dirty detached view is not checked until reattach() or a direct detectChanges() call.

This advanced escape hatch can support a deliberately throttled local rendering boundary, but it creates a second lifecycle that must handle teardown, hidden state, inputs, errors and tests. Profile first. For most cases, OnPush, Signals, stream rate control and better algorithms are simpler.

15. Why should dynamically created components use setInput?

Directly assigning an input property through a component instance can bypass normal input bookkeeping and does not automatically mark an OnPush component. ComponentRef.setInput() participates in Angular input semantics and is a documented zoneless notification.

Similarly, if @ViewChild gives a parent a child instance, avoid mutating the child's input property as an imperative data channel. Prefer template bindings or setInput() for a dynamic ComponentRef.


Rendering performance

16. How should lists be tracked with @for?

Modern Angular uses a required track expression in @for:

@for (user of users(); track user.id) {
  <app-user-row [user]="user" />
} @empty {
  <p>No users</p>
}

Use a stable unique domain key. $index fits only static collections that are not reordered or edited by identity. track item uses reference identity and can recreate excessive DOM when every refresh creates new objects. Tracking lets Angular relate data to views; it does not guarantee that only “changed items render” under every parent notification.

17. Are pure pipes always better than template methods?

No. Angular evaluates template expressions whenever their view is checked, so an expensive method can be a hotspot. A pure pipe caches its most recent result based on input identity and may help; a computed signal or domain-level memoization may fit other ownership models.

Cheap, pure methods can be perfectly acceptable. Caches cost memory and equality work and can hide stale results when mutable inputs retain identity. Use Angular DevTools to identify an actual slow computation before adding memoization.

18. What causes ExpressionChangedAfterItHasBeenCheckedError?

In development checks, Angular can detect that a binding changed after it had already been checked in the same stabilization process. Common causes include mutating parent-visible state from a child lifecycle hook, deriving state through side effects during rendering, or reading a value whose getter changes data.

Do not “fix” the design by adding setTimeout() or unconditional detectChanges(). Move derivation into pure state, update in the event that owns the change, use an appropriate render callback for DOM work, or correct the one-way data-flow boundary.


Debugging and testing

19. How do you diagnose Angular change-detection performance?

Start with a reproducible slow interaction and a budget. Record Angular DevTools or the Chrome DevTools Angular performance track and ask:

  • what notification scheduled the pass;
  • which views were checked or skipped;
  • which template or lifecycle computation dominated time;
  • whether repeated passes indicate state changes during change detection;
  • whether the real cost is JavaScript, layout, paint or third-party code.

Then change one cause: stop a noisy schedule, use stable list identity, optimize the algorithm, move expensive work to a measured cache, narrow state dependencies, or migrate a compatible boundary to zoneless/OnPush. Re-record the same scenario.

20. How do you test OnPush and zoneless behavior?

Configure tests to resemble production; provideZonelessChangeDetection() can be added to TestBed when compatibility is under test. Drive state through public inputs, bound events, signals or services, then await fixture stability or call fixture APIs deliberately.

Include regression tests for the failure mode: same-reference input mutation, dynamic setInput(), Observable emission, detached view or third-party callback. Avoid tests that pass only because fixture.detectChanges() is called after every statement; that can conceal a missing production notification.


Quick reference

ConceptCurrent behavior
Default strategy in Angular 22OnPush
EagerChecks a view whenever traversal reaches it
Default enum memberDeprecated alias for Eager
Default scheduler in Angular 21+Zoneless
Template-read signal updateMarks dependent view and notifies scheduler
markForCheck()Marks/schedules a normal future traversal
detectChanges()Synchronous local check of view and children
detach()Removes view from normal traversal until reattached
@for ... trackMaintains item-to-view identity

Frequently Asked Questions

What is Angular change detection?

Change detection evaluates bindings in eligible views and updates the DOM when rendered values differ. Scheduling and traversal are separate: Angular 21+ schedules work from zoneless framework notifications, while each component's OnPush or Eager strategy determines whether its subtree is eligible during a traversal.

What changed for change detection in Angular 22?

OnPush became the default strategy in Angular 22. The previous Default strategy is now named Eager; Default remains a deprecated alias. Zoneless scheduling was already the default from Angular 21, so current code should explain explicit notifications instead of assuming Zone.js triggers a global pass after every asynchronous callback.

When is an OnPush component checked?

Angular checks an OnPush view when a bound input changes, an event is handled in its subtree, or the view is marked through a supported notification such as markForCheck, AsyncPipe, or a template-read signal update. ComponentRef.setInput also notifies Angular. Events can cause ancestor views to be checked while unrelated clean OnPush subtrees are skipped.

What is the difference between markForCheck and detectChanges?

markForCheck marks a view so Angular includes it in a scheduled change-detection traversal; in zoneless mode it is also a scheduling notification. detectChanges synchronously checks that view and its children locally. Prefer normal notifications and markForCheck; reserve detectChanges for explicit local-check boundaries, commonly together with detach.

Why can mutating an input break an OnPush view?

If a parent mutates an object already passed to a child and no other notification makes that child eligible, the bound input identity has not changed and the child may be skipped. Replace the value at the ownership boundary or expose reactive state. A mutation inside the child's own bound event may still render because that event marks its subtree, so the slogan mutation never updates is false.

How should Angular change detection performance be optimized?

Start with Angular DevTools or the Chrome Angular performance track and identify excessive scheduling, an unexpectedly checked subtree, slow template or lifecycle work, or DOM layout cost. Then fix the measured cause with zoneless-compatible notifications, stable list keys, faster algorithms, pure pipes or memoization, smaller reactive boundaries, and re-profile.


Sources

Ready to ace your interview?

Get 550+ interview questions with detailed answers in our comprehensive PDF guides.

View PDF Guides