45+ Angular Change Detection Interview Questions 2025: OnPush, Zone.js & Performance

·12 min read
angularinterview-questionschange-detectionperformancefrontend

Angular's default change detection checks every component on every browser event—click, keypress, HTTP response, setTimeout. In an application with 500 components, that's 500 checks per interaction. Understanding OnPush and immutability can reduce this to checking only the components that actually changed. This guide covers the change detection questions that dominate Angular interviews.

Table of Contents

  1. Change Detection Fundamentals Questions
  2. OnPush Strategy Questions
  3. Zone.js Questions
  4. ChangeDetectorRef Questions
  5. Performance Optimization Questions
  6. Debugging Questions
  7. Quick Reference

Change Detection Fundamentals Questions

These questions test your understanding of how Angular keeps the view synchronized with component data.

What is Angular change detection and how does it work?

Change detection is Angular's mechanism for keeping the view in sync with the component's data. When data changes, Angular walks through the entire component tree, checks each component's bindings, and updates the DOM where values have changed.

By default, Angular uses Zone.js to detect async events—like user input, HTTP responses, and timers—and then automatically triggers change detection. This means developers don't need to manually call update methods; Angular handles it transparently.

What are the two change detection strategies in Angular?

Angular provides two change detection strategies: Default and OnPush. They differ significantly in when Angular checks a component for changes.

With Default strategy, every async event—HTTP response, click, timer—triggers change detection across the entire component tree. Angular checks every binding in every component, which can become expensive in large applications.

OnPush is more restrictive and performant. A component only checks when an @Input reference changes (not mutations), an event fires from the component or its children, you manually call detectChanges() or markForCheck(), or an async pipe receives a new value.

How does Default change detection work?

Default change detection runs on every async event in the application. Angular starts at the root component and walks down the entire tree, checking every binding in every component.

@Component({
  selector: 'app-user',
  template: `
    <div>{{ user.name }}</div>
    <button (click)="updateName()">Update</button>
  `
  // Default: changeDetection: ChangeDetectionStrategy.Default
})
export class UserComponent {
  user = { name: 'John' };
 
  updateName() {
    this.user.name = 'Jane';  // Works - mutation triggers update
  }
}

With Default strategy, mutations work because Angular checks every binding on every cycle, regardless of whether references changed.

How does OnPush change detection work?

OnPush change detection only runs when specific conditions are met: input references change, events originate from the component, or manual triggers are called. This dramatically reduces unnecessary checks.

@Component({
  selector: 'app-user',
  template: `
    <div>{{ user.name }}</div>
    <button (click)="updateName()">Update</button>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserComponent {
  @Input() user!: User;
 
  // This WON'T update the view with OnPush!
  updateNameWrong() {
    this.user.name = 'Jane';  // Same reference - no detection
  }
 
  // This WILL update the view
  updateNameCorrect() {
    this.user = { ...this.user, name: 'Jane' };  // New reference
  }
}

The key insight is that OnPush uses strict equality (===) to compare input references. Same reference means "no change" to Angular.


OnPush Strategy Questions

OnPush is essential for performance optimization. These questions test your ability to work with it correctly.

Why does mutating an object not trigger OnPush change detection?

OnPush change detection checks if the @Input reference has changed, not if the content has changed. When you mutate an object—like array.push() or object.property = value—the reference stays the same.

Angular's OnPush comparison uses strict equality (===). The same reference means "no change," so Angular skips the component entirely. This is why immutable patterns are essential with OnPush.

What happens when you push to an array with OnPush?

This is a classic interview scenario that trips up even experienced Angular developers. Consider this component:

@Component({
  selector: 'app-list',
  template: `
    <ul>
      <li *ngFor="let item of items">{{ item.name }}</li>
    </ul>
    <button (click)="addItem()">Add Item</button>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListComponent {
  @Input() items: Item[] = [];
 
  addItem() {
    // BUG: This won't trigger change detection!
    this.items.push({ name: 'New Item' });
  }
}

The view won't update because push() mutates the array but keeps the same reference. OnPush sees the same reference and assumes nothing changed.

How do you fix OnPush components that aren't updating?

There are three main approaches to fix OnPush components that aren't updating after data changes.

Create a new reference (Recommended):

addItem() {
  this.items = [...this.items, { name: 'New Item' }];
}

Use detectChanges() for immediate update:

constructor(private cdr: ChangeDetectorRef) {}
 
addItem() {
  this.items.push({ name: 'New Item' });
  this.cdr.detectChanges();  // Force immediate check
}

Use markForCheck() to schedule update:

constructor(private cdr: ChangeDetectorRef) {}
 
addItem() {
  this.items.push({ name: 'New Item' });
  this.cdr.markForCheck();  // Schedule for next cycle
}

The immutable approach is preferred because it makes change detection predictable and keeps the component pure.

Can you have too many OnPush components?

OnPush is almost always better for performance. The only consideration is that it requires immutable patterns, which adds some code complexity. But the performance benefit of skipping unnecessary checks almost always outweighs the extra code.

The real issue is mixing OnPush and Default inconsistently. The recommended approach is making OnPush the default for all components and only using Default when there's a specific reason.


Zone.js Questions

Zone.js is the magic behind Angular's automatic change detection. These questions test senior-level understanding.

What is Zone.js and why does Angular use it?

Zone.js is a library that patches all async APIs—setTimeout, Promise, addEventListener, fetch, requestAnimationFrame—to notify Angular when async operations complete. This allows Angular to automatically trigger change detection without developers manually calling update methods.

Zone.js creates an execution context that tracks async operations across their lifecycle. When an async operation completes, Zone.js notifies Angular, which then runs change detection.

// All of these trigger change detection automatically:
setTimeout(() => { /* ... */ });      // Patched
setInterval(() => { /* ... */ });     // Patched
Promise.resolve().then(() => {});     // Patched
fetch('/api/data');                   // Patched
element.addEventListener('click');   // Patched
requestAnimationFrame(() => {});      // Patched

What is NgZone and when would you use it?

NgZone is Angular's wrapper around Zone.js that provides hooks into the change detection cycle. It's used when you need explicit control over when change detection runs.

The main use cases are runOutsideAngular() to run code without triggering change detection (useful for performance-critical code like animations), run() to explicitly trigger change detection when you need to update the UI from outside Angular's zone, and onStable to react when Angular finishes all tasks (useful for integration testing).

How do you run code outside Angular's zone?

For high-frequency operations like animations, scroll handlers, or mouse tracking, you want to avoid triggering change detection on every event. Use runOutsideAngular():

@Component({
  selector: 'app-performance',
  template: `<canvas #canvas></canvas>`
})
export class PerformanceComponent {
  constructor(private ngZone: NgZone) {}
 
  startAnimation() {
    // Runs outside Angular - no change detection triggered
    this.ngZone.runOutsideAngular(() => {
      this.animationLoop();
    });
  }
 
  animationLoop() {
    // High-frequency updates without triggering change detection
    this.drawFrame();
    requestAnimationFrame(() => this.animationLoop());
  }
 
  updateUI(value: string) {
    // When you DO need to update Angular bindings
    this.ngZone.run(() => {
      this.displayValue = value;
    });
  }
}

This pattern prevents change detection on every animation frame while still allowing UI updates when needed.


ChangeDetectorRef Questions

ChangeDetectorRef provides manual control over change detection. These questions test practical usage.

What is the difference between detectChanges() and markForCheck()?

detectChanges() runs change detection immediately and synchronously on the component and its children. The view updates right away. markForCheck() marks the component and all ancestors up to the root for checking in the next change detection cycle—it doesn't run detection immediately.

// detectChanges - immediate, synchronous
this.value = newValue;
this.cdr.detectChanges();  // View updates NOW
 
// markForCheck - scheduled, batched
this.value = newValue;
this.cdr.markForCheck();  // View updates on next cycle
// ... other code runs first

Use detectChanges() when you need immediate visual feedback, like after receiving WebSocket data. Use markForCheck() when timing isn't critical and you want to batch updates.

How do you use ChangeDetectorRef with external data sources?

When data comes from sources that don't trigger Angular's change detection automatically (like WebSocket messages or third-party libraries), you need to manually notify Angular:

@Component({
  selector: 'app-dashboard',
  template: `<div>{{ data }}</div>`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class DashboardComponent {
  data: string = '';
 
  constructor(private cdr: ChangeDetectorRef) {}
 
  // External data source not using @Input
  onWebSocketMessage(message: string) {
    this.data = message;
 
    // Option 1: Immediate update
    this.cdr.detectChanges();
 
    // Option 2: Schedule for next cycle
    this.cdr.markForCheck();
  }
}

When should you detach the change detector?

You can completely detach a component from change detection for components that display static content after initial render:

@Component({
  selector: 'app-static-content',
  template: `<div>{{ content }}</div>`
})
export class StaticContentComponent implements AfterViewInit {
  @Input() content!: string;
 
  constructor(private cdr: ChangeDetectorRef) {}
 
  ngAfterViewInit() {
    // Component will never check again unless reattached
    this.cdr.detach();
  }
 
  forceUpdate() {
    this.cdr.detectChanges();  // Manual update when needed
  }
}

This is useful for components displaying large amounts of static data where you want to eliminate all change detection overhead after the initial render.


Performance Optimization Questions

These questions test your knowledge of practical optimization patterns.

How does trackBy improve ngFor performance?

Without trackBy, Angular destroys and recreates all DOM elements when the array changes. With trackBy, Angular only updates elements whose tracked value changed:

@Component({
  selector: 'app-user-list',
  template: `
    <div *ngFor="let user of users; trackBy: trackByUserId">
      {{ user.name }}
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserListComponent {
  @Input() users: User[] = [];
 
  trackByUserId(index: number, user: User): number {
    return user.id;  // Only re-render items that changed
  }
}

This is especially important for lists that update frequently or contain complex child components.

Why should you use async pipe instead of subscribing in ngOnInit?

The async pipe provides several advantages over manual subscription with OnPush components:

@Component({
  selector: 'app-data',
  template: `
    <div *ngIf="data$ | async as data">
      {{ data.value }}
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class DataComponent {
  data$ = this.dataService.getData();
 
  constructor(private dataService: DataService) {}
}

The async pipe automatically subscribes and unsubscribes (preventing memory leaks), automatically calls markForCheck() on new values, and works perfectly with OnPush. With manual subscription, you must call markForCheck() yourself and remember to unsubscribe in ngOnDestroy.

Why should you use pure pipes instead of methods in templates?

Methods in templates are called on every change detection cycle, even if the input hasn't changed. Pure pipes are memoized—they only recalculate when the input changes:

// BAD: Method called on every change detection
@Component({
  template: `<div>{{ formatDate(date) }}</div>`
})
export class BadComponent {
  formatDate(date: Date): string {
    return date.toLocaleDateString();  // Called every cycle!
  }
}
 
// GOOD: Pure pipe only recalculates when input changes
@Pipe({ name: 'formatDate', pure: true })
export class FormatDatePipe implements PipeTransform {
  transform(date: Date): string {
    return date.toLocaleDateString();
  }
}
 
@Component({
  template: `<div>{{ date | formatDate }}</div>`
})
export class GoodComponent {}

For expensive transformations, the performance difference can be significant.


Debugging Questions

These questions test your ability to diagnose and fix change detection issues.

How would you debug change detection issues?

Follow a systematic approach when debugging change detection problems. First, check if using OnPush—are inputs changing by reference or just being mutated? Add console.log in the template to see how often it renders. Use Angular DevTools to visualize the change detection tree. Check if async operations are running outside Zone.js. Verify that ChangeDetectorRef methods are called correctly.

The most common issues are mutations with OnPush, forgotten async pipe subscriptions, and code running outside Angular's zone.

Why might a component with OnPush not update even when data changes?

There are several common causes. The input reference didn't change (mutation instead of new object). An async operation is running outside Angular's zone. The change came from a service or WebSocket without triggering change detection. A parent component with Default strategy passed mutated data down.

To fix, ensure you're creating new references for @Input data, use async pipe for observables, or call markForCheck() or detectChanges() manually when receiving data from external sources.

What happens with setInterval in an OnPush component?

Consider this example:

@Component({
  template: `<div>{{ counter }}</div>`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent implements OnInit {
  counter = 0;
 
  ngOnInit() {
    setInterval(() => {
      this.counter++;
      console.log(this.counter);
    }, 1000);
  }
}

The console logs incrementing numbers (1, 2, 3...), but the view stays at 0. OnPush doesn't check because no input changed and no event came from the template. The fix is to inject ChangeDetectorRef and call markForCheck() inside the interval, or convert to an observable and use async pipe.


Quick Reference

ConceptKey Points
Default StrategyChecks on every async event, entire tree
OnPush StrategyChecks only on input change, events, or manual trigger
detectChanges()Immediate, synchronous check
markForCheck()Schedule check for next cycle
Zone.jsPatches async APIs to auto-trigger detection
runOutsideAngular()High-frequency code without detection
async pipeAuto-subscribe + markForCheck on new values
trackByPrevent full list re-renders
Pure pipesMemoized transforms, better than methods

Ready to ace your interview?

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

View PDF Guides