Angular Change Detection
โฑ๏ธ ~5-minute bite ยท solve the sandbox to master
5-Year-Old Metaphor
โ The physical, real-world picture. No jargon.๐จ Default CD = fire alarm goes off, everyone evacuates the building to check if it's real. OnPush = only the floor that reported smoke gets checked. Signals = the specific smoke detector in room 304 alerts directly โ no building-wide evacuation.
๐
Default
Every component checked after every async event. Simple, correct, can be slow at scale.
๐ฏ
OnPush
Only check when @Input refs change, async pipe emits, or markForCheck() called. 90%+ reduction possible.
โก
Signals
Fine-grained: only the template expressions that read a changed signal are re-evaluated. Zoneless future.
zone.js โ what it monkey-patches
After any of these complete, zone.js notifies Angular: โsomething async finished โ time to check for changes.โ Angular then runs a full CD cycle from the root.
Interactive Sandbox
โ Move something, see it react instantly.Pattern
Component tree โ CD blast radius
When to use
Small-to-medium apps, prototyping, any component that doesn't need to optimize. The default. Zone.js removes the need to think about change detection in most cases.
| 1 | // zone.js is bootstrapped automatically in main.ts |
| 2 | // It wraps these APIs to trigger CD after each one completes: |
| 3 | // setTimeout / setInterval / clearTimeout / clearInterval |
| 4 | // XMLHttpRequest (open, send, abort) |
| 5 | // Promise (.then, .catch, .finally) |
| 6 | // EventTarget (addEventListener, removeEventListener) |
| 7 | // MutationObserver / IntersectionObserver |
| 8 | ย |
| 9 | // No code needed โ this is the default behavior |
| 10 | @Component({ |
| 11 | selector: 'app-counter', |
| 12 | template: `<p>{{ count }}</p><button (click)="inc()">+</button>`, |
| 13 | // changeDetection: ChangeDetectionStrategy.Default (implicit) |
| 14 | }) |
| 15 | export class CounterComponent { |
| 16 | count = 0; |
| 17 | inc() { this.count++; } |
| 18 | // When inc() fires: |
| 19 | // 1. Click event completes |
| 20 | // 2. zone.js notifies Angular: "async work done" |
| 21 | // 3. Angular CD runs from the root component down |
| 22 | // 4. Every component in the tree is dirty-checked |
| 23 | // 5. Any binding that changed gets re-rendered |
| 24 | } |
| 25 | ย |
| 26 | // For large apps, this "check everything" approach becomes slow |
| 27 | // Solution: ChangeDetectionStrategy.OnPush on heavy components |
Challenge
Explore all 5 CD patterns. Use the tree to see the blast radius of a change in Default vs OnPush vs Signals.
Why Should I Care?
โ The exact interview question + the bug it kills.Interview questions
Q: Why does OnPush sometimes miss updates?
OnPush checks reference equality โ it only re-renders when an @Input reference changes. If you mutate an object or array that was passed as @Input (e.g., pushing to an array), the reference stays the same and Angular skips the component. The fix: always use immutable updates โ spread operators, Array.from, Object.assign into a new object โ so the parent passes a new reference.
Q: What does zone.js actually do?
zone.js monkey-patches the browser's async APIs at startup โ replacing setTimeout, Promise.then, XMLHttpRequest, and others with wrapper versions that call the original and then notify Angular. This is how Angular knows โasync work finished, time to check for changesโ without you writing any notification code. The cost: any async event, even unrelated ones, triggers a full CD cycle.
Q: How do Angular signals bypass zone.js?
Signals maintain a producer/consumer graph. When you call signal.set(), the signal notifies its consumers (computed signals and template expressions that read it) directly through Angular's scheduler โ no zone.js involved. Angular marks only the affected template expressions as dirty and schedules a microtask to re-render them. This is how zoneless Angular (Angular 18+) works: remove zone.js entirely and rely on signals to drive all CD.
The Deep Dive
โ Spec refs, engine internals, the minutiae.ChangeDetectorRef API
async pipe + OnPush โ the canonical pattern
The async pipe is aware of OnPush. When an Observable emits a new value through the async pipe, it automatically calls markForCheck() โ no manual wiring needed. This is the recommended pattern for OnPush components that consume reactive data. The async pipe also handles unsubscribe on destroy, preventing memory leaks.
| 1 | @Component({ |
| 2 | changeDetection: ChangeDetectionStrategy.OnPush, |
| 3 | template: ` |
| 4 | <div *ngIf="data$ | async as data; else loading"> |
| 5 | {{ data.title }} |
| 6 | </div> |
| 7 | <ng-template #loading>Loading...</ng-template> |
| 8 | `, |
| 9 | }) |
| 10 | export class DataComponent { |
| 11 | data$ = this.service.getData(); // Observable<Data> |
| 12 | // async pipe: subscribes, triggers markForCheck() on each emission, |
| 13 | // unsubscribes on destroy. Zero manual CD code. |
| 14 | constructor(private service: DataService) {} |
| 15 | } |
Zoneless Angular (Angular 18+)
provideExperimentalZonelessChangeDetection() removes zone.js from the app entirely. All CD must be driven by: signals (the primary driver), markForCheck() / detectChanges() on ChangeDetectorRef, or AsyncPipe emitting. Benefits: smaller bundle (zone.js is ~60KB), faster initial load, no monkey-patching surprises, better compatibility with native async/await. Migration path: add OnPush to all components first, replace Observable subscriptions with async pipe or toSignal(), then switch to zoneless.
Interview Questions
โ Real questions from real interviews โ with answers.zone.js monkey-patches async APIs so Angular knows when async work is done and should check for changes.
@Input ref change, event from within, async pipe emits, or markForCheck() is called.
markForCheck() schedules the component for the next CD cycle; detectChanges() runs CD synchronously right now.
detach() removes the component from the CD tree entirely โ no checks at all until reattach() is called.
Signals maintain a producer/consumer graph โ a signal change directly notifies only the template expressions that read it.
Apply OnPush to all components first, replace subscriptions with async pipe or toSignal(), then switch provideExperimentalZonelessChangeDetection().
Memory Game
โ Quick quiz โ lock the concept in long-term memory.What does trackBy in ngFor do for change detection?