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 knowledge gap is exactly why change detection dominates Angular interview questions.
The 30-Second Answer
When the interviewer asks "How does Angular change detection work?", here's your concise answer:
"Change detection is how Angular keeps the view in sync with the component's data. By default, Angular uses Zone.js to detect async events and then walks through the entire component tree, checking each binding and updating the DOM where needed. You can optimize this with OnPush strategy, which only checks a component when its inputs change by reference or events occur within it."
Wait for follow-up questions. Don't dive into Zone.js internals unless they ask.
The 2-Minute Answer (If They Want More)
If they ask you to elaborate:
"Angular has two change detection strategies: Default and OnPush.
With Default, every async event - HTTP response, click, timer - triggers change detection across the entire component tree. Angular checks every binding in every component.
OnPush is more restrictive. 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()
- An async pipe receives a new value
Zone.js is the magic behind automatic detection - it patches async APIs to notify Angular when they complete.
For performance, I use OnPush on most components combined with immutable data patterns. This can dramatically reduce the number of checks Angular performs."
Code Examples to Show
Default Change Detection
@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
}
}OnPush Change Detection
@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
}
}Using ChangeDetectorRef
@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();
}
}The Classic Problem: Mutating Objects with OnPush
This scenario trips up even experienced Angular developers:
Interviewer: "Your component uses OnPush but the view isn't updating when you change the data. What's happening?"
The Bug
@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' });
}
}What's happening? push() mutates the array but keeps the same reference. OnPush uses === comparison - same reference means "no change".
Three Ways to Fix It
1. Create a new array reference (Recommended)
addItem() {
this.items = [...this.items, { name: 'New Item' }];
}2. Use detectChanges() for manual trigger
constructor(private cdr: ChangeDetectorRef) {}
addItem() {
this.items.push({ name: 'New Item' });
this.cdr.detectChanges(); // Force immediate check
}3. Use markForCheck() to schedule update
constructor(private cdr: ChangeDetectorRef) {}
addItem() {
this.items.push({ name: 'New Item' });
this.cdr.markForCheck(); // Schedule for next cycle
}The lesson: "With OnPush, always treat data as immutable. Create new references instead of mutating. This makes change detection predictable and performant."
Zone.js Deep Dive (Senior Level Question)
Interviewers love asking about Zone.js because it separates those who understand Angular internals from those who don't.
What Zone.js Patches
// All of these trigger change detection automatically:
setTimeout(() => { /* ... */ }); // Patched
setInterval(() => { /* ... */ }); // Patched
Promise.resolve().then(() => {}); // Patched
fetch('/api/data'); // Patched
element.addEventListener('click'); // Patched
requestAnimationFrame(() => {}); // PatchedRunning Outside Angular's Zone
@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;
});
}
}When to use: "I use runOutsideAngular for high-frequency operations like animations, scroll handlers, or mouse tracking where I don't need Angular to update on every event. Then I use ngZone.run() when I actually need to update the UI."
Practical Optimization Patterns
1. TrackBy for ngFor
@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
}
}Without trackBy: Angular destroys and recreates all DOM elements when the array changes. With trackBy: Angular only updates elements whose tracked value changed.
2. Async Pipe (Best Practice)
@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) {}
}Why it's powerful:
- Automatically subscribes/unsubscribes (no memory leaks)
- Automatically calls markForCheck() on new values
- Works perfectly with OnPush
3. Pure Pipes vs Methods
// 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 {}4. Detaching Change Detector
@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
}
}Use case: Components that display static content after initial render.
Common Follow-Up Questions
"What's the difference between detectChanges() and markForCheck()?"
"
detectChanges()runs change detection immediately and synchronously on this component and its children.markForCheck()marks this component and all ancestors up to the root for checking in the next cycle - it doesn't run detection immediately.I use
detectChanges()when I need immediate visual feedback, like after receiving WebSocket data. I usemarkForCheck()when timing isn't critical and I want to batch updates."
// 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"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. The main use cases are:
runOutsideAngular()- Run code without triggering change detection, useful for performance-critical code like animationsrun()- Explicitly trigger change detection, useful when you need to update the UI from outside Angular's zoneonStable- React when Angular finishes all tasks, useful for integration testing"
"How would you debug change detection issues?"
"I follow these steps:
- Check if using OnPush - are inputs changing by reference?
- 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."
"Can you have too many OnPush components?"
"Not really - OnPush is almost always better for performance. The only consideration is that it requires immutable patterns, which adds some complexity. But the performance benefit of skipping unnecessary checks almost always outweighs the extra code.
The real issue is mixing OnPush and Default inconsistently. I prefer making OnPush the default for all components and only using Default when there's a specific reason."
What Interviewers Are Really Testing
When I ask about change detection, I'm checking:
- Conceptual understanding - Do you know how Angular updates the DOM?
- Strategy knowledge - Can you explain Default vs OnPush?
- Problem-solving - Can you diagnose why a component isn't updating?
- Performance awareness - Do you know optimization techniques?
- Deep knowledge - Do you understand Zone.js and NgZone?
A candidate who explains both strategies, shows the immutable pattern fix, and mentions async pipe and trackBy will stand out.
Quick Reference Card
| Concept | Key Points |
|---|---|
| Default Strategy | Checks on every async event, entire tree |
| OnPush Strategy | Checks only on input change, events, or manual trigger |
| detectChanges() | Immediate, synchronous check |
| markForCheck() | Schedule check for next cycle |
| Zone.js | Patches async APIs to auto-trigger detection |
| runOutsideAngular() | High-frequency code without detection |
| async pipe | Auto-subscribe + markForCheck on new values |
| trackBy | Prevent full list re-renders |
| Pure pipes | Memoized transforms, better than methods |
Practice Questions
Test yourself before your interview:
1. What's the output and why?
@Component({
template: `<div>{{ counter }}</div>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent implements OnInit {
counter = 0;
ngOnInit() {
setInterval(() => {
this.counter++;
console.log(this.counter);
}, 1000);
}
}2. Why won't this component update and how do you fix it?
@Component({
template: `<div *ngFor="let item of items">{{ item }}</div>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListComponent {
@Input() items: string[] = [];
addItem(newItem: string) {
this.items.push(newItem);
}
}3. When would you use runOutsideAngular()? Give a practical example.
4. What's the difference between using async pipe vs subscribing in ngOnInit with OnPush?
Answers:
-
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. Fix: inject ChangeDetectorRef and call
markForCheck()inside the interval. -
push()mutates the array without changing its reference. OnPush only checks when references change. Fix:this.items = [...this.items, newItem]or calldetectChanges(). -
Use for high-frequency operations like scroll handlers, animations, or mouse tracking. Example:
ngZone.runOutsideAngular(() => window.addEventListener('mousemove', this.handleMouseMove))- prevents change detection on every pixel moved. -
With async pipe: automatic subscription management, automatic markForCheck(), no memory leak risk. With manual subscribe: must call markForCheck() yourself, must unsubscribe in ngOnDestroy, easy to forget either one. Async pipe is the recommended pattern.
Related Articles
If you found this helpful, check out these related guides:
- Complete Frontend Developer Interview Guide - comprehensive preparation guide for frontend interviews
- Angular RxJS Interview Guide - Reactive programming patterns with RxJS
- 7 Advanced Angular Interview Questions - Advanced patterns, performance, and architecture
- Angular 21 Interview Guide - Latest Angular features and improvements
Ready for More Angular Interview Questions?
This is just one topic from our complete Angular interview prep guide. Get access to 50+ Angular questions covering:
- Components and Directives
- Dependency Injection
- RxJS and Observables
- Routing and Guards
- Forms (Template-driven and Reactive)
- Testing strategies
Get Full Access to All Angular Questions →
Or try our free Angular preview to see more questions like this.
Written by the EasyInterview team, based on real interview experience from 12+ years in tech and hundreds of technical interviews conducted at companies like BNY Mellon, UBS, and leading fintech firms.
