Suggest an editImprove this articleRefine the answer for “How does change detection work in Angular?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Change detection** in Angular is the process that walks the component tree after Zone.js catches an async event and diffs each DOM binding against its last recorded value. ```typescript @Component({ changeDetection: ChangeDetectionStrategy.OnPush, template: `<p>{{ user.name }}</p>` }) export class UserComponent { @Input() user!: User; // re-checked only when reference changes } ``` **Key point:** Default checks every component on every tick. OnPush skips subtrees unless an `@Input()` reference changes or an Observable emits, cutting checks by ~80% in large apps.Shown above the full answer for quick recall.Answer (EN)Image**Change detection** in Angular is the mechanism that walks the component tree and syncs DOM bindings when application state changes. ## Theory ### TL;DR - Zone.js patches `setTimeout`, `fetch`, and `addEventListener` to detect async activity and trigger a tree walk automatically - Default strategy checks every component on every event. OnPush skips a subtree unless its `@Input()` reference changes or an Observable emits - OnPush + `async` pipe cuts the number of checks by roughly 80% in apps with 50+ components - Angular compares by reference, not by value. Mutating an object won't trigger OnPush ### Quick example ```typescript import { Component } from '@angular/core'; @Component({ selector: 'app-counter', template: `<button (click)="count++">{{ count }}</button>` }) export class CounterComponent { count = 0; } ``` Click fires a DOM event. Zone.js intercepts it and schedules `ApplicationRef.tick()`, which walks the tree from root. Angular finds `{{ count }}` changed from `0` to `1` and patches the DOM. That is the whole cycle. ### How Zone.js triggers the cycle Angular does not watch data directly. Zone.js patches browser APIs: `addEventListener`, `setTimeout`, `setInterval`, `XMLHttpRequest`, `fetch`, and `Promise`. Any time one of these fires inside the Angular zone, it queues a microtask. When that microtask runs, `NgZone.onMicrotaskEmpty` fires and calls `ApplicationRef.tick()`. `tick()` starts at the root ViewRef (in Ivy, an LView) and walks down the entire tree. For each component view, Angular runs lifecycle hooks, diffs each binding against its last recorded value, and patches the DOM if anything changed. Ivy optimizes this with 8-byte flag fields on each LView to skip views already marked as clean. ### Default vs OnPush Default checks every component on every tick, no exceptions. A click anywhere in the app triggers checks on all 200 components, even if 195 have nothing to do with that click. OnPush changes the rule. Angular skips a component and its subtree unless one of these is true: an `@Input()` reference changed, an event originated inside that component, an Observable pushed a value via `async` pipe, or you called `markForCheck()` manually. The numbers tell the story: 100 components with Default means 100 checks per click. With OnPush and only 5 dirty inputs, it is 5 checks. ### When to use each strategy - Small app, under 10 components: Default works fine, zero config needed - List or grid with 100+ rows: OnPush + `trackBy` in `*ngFor` - NgRx or Observable-driven app: OnPush + `async` pipe everywhere - Frequent WebSocket updates or timers: `ChangeDetectorRef.markForCheck()` inside the subscription ### How Angular compares values Angular uses reference equality (`===`), not deep comparison. This matters most for OnPush. ```typescript // OnPush sees the same object reference - no update this.user.name = 'Bob'; // New reference - OnPush triggers a check this.user = { ...this.user, name: 'Bob' }; ``` Immutable updates are the standard pattern in OnPush components for this reason. Same array reference? No check. New array from spread or `.concat()`? Check runs. ### Common mistakes **Mutating @Input objects in a child component** ```typescript // Parent passes user object, child mutates it directly this.user.name = 'Bob'; // OnPush child never re-renders ``` OnPush compares object references. Mutation is invisible to it. Always return a new object or array from the parent. **Using setTimeout for state updates** ```typescript setTimeout(() => this.data = newData, 0); // works but triggers full tree walk ``` This patches the whole tree every time. At scale it hurts. Use `markForCheck()` inside an OnPush component with Observable-driven state instead. **Detaching everywhere for "performance"** ```typescript constructor(private cdr: ChangeDetectorRef) { this.cdr.detach(); // this component no longer auto-checks } ``` Detached components need manual `detectChanges()` calls or the UI freezes on real data. Use detach only in OnPush leaf components with no user-facing events, and call `detectChanges()` sparingly. **Calling functions in templates** ```html <div *ngFor="let item of getFilteredItems()">{{ item }}</div> ``` `getFilteredItems()` runs on every change detection cycle. With a large list at 60fps, this tanks performance. Pre-compute the result in `ngOnInit` or use a `pure: true` pipe. ### Real-world usage - Angular Material table: OnPush + `trackBy` for virtual scroll with 10k rows - NgRx: `async` pipe in OnPush components as the default pattern in official docs - AngularFire: Zone.js patches Firebase realtime listeners automatically, no manual triggers needed - Nx monorepos: Default for lazy-loaded feature modules, OnPush for shared lib components ### Follow-up questions **Q:** What exactly does Zone.js patch and why does Angular need it? **A:** Zone.js patches `addEventListener`, `setTimeout`, `setInterval`, `XMLHttpRequest`, `fetch`, and `Promise`. Angular has no built-in way to know when something async happened inside component classes. Zone.js provides that signal, and Angular decides what to re-check. **Q:** What happens in apps running without Zone.js? **A:** Change detection must be triggered manually with `ChangeDetectorRef.detectChanges()` or `markForCheck()`. Angular Signals (Angular 16+) are the cleaner path: they notify Angular of specific changes without walking the whole tree. **Q:** Default vs OnPush in numbers? **A:** With 100 components and Default, every click runs 100 checks. With OnPush and 5 dirty inputs, it runs 5. Angular team benchmarks show around 80% fewer cycles in component-heavy dashboards. **Q:** How does Ivy change detection differ from ViewEngine? **A:** Ivy stores LView flags as tagged 8-byte pointers, letting it skip clean views in 2 instructions instead of traversing their whole structure. ViewEngine dirtied entire views. Angular 17+ Signals go further: fine-grained reactivity with no tree walk at all. **Q:** Why use `async` pipe instead of manual `subscribe()`? **A:** `async` pipe auto-unsubscribes on component destroy and calls `markForCheck()` automatically. With manual `subscribe()` in an OnPush component, you need to call `markForCheck()` yourself or the view never updates. ## Examples ### Basic counter with Default strategy ```typescript @Component({ selector: 'app-counter', template: ` <p>Count: {{ count }}</p> <button (click)="increment()">+</button> ` }) export class CounterComponent { count = 0; increment() { this.count++; // Zone.js catches the click, tree walk starts } } ``` Zone.js catches the click, `tick()` runs, Angular finds `{{ count }}` changed, DOM updates. With Default strategy, every component in the app gets checked on this single click. ### Todo list with OnPush and async pipe ```typescript @Component({ selector: 'app-todos', changeDetection: ChangeDetectionStrategy.OnPush, template: ` <ul> <li *ngFor="let todo of todos$ | async; trackBy: trackById"> {{ todo.title }} </li> </ul> ` }) export class TodosComponent { todos$ = this.todoService.todos$; // Observable from HttpClient trackById(index: number, todo: Todo) { return todo.id; // stable identity for DOM node reuse } constructor(private todoService: TodoService) {} } ``` `async` pipe subscribes to the Observable and calls `markForCheck()` when a new value arrives. With `trackBy`, Angular reuses existing DOM nodes for unchanged items. 100 todos load once; only changed items re-render. ### OnPush with immutable updates ```typescript @Component({ changeDetection: ChangeDetectionStrategy.OnPush, template: `<p>{{ user.name }}</p>` }) export class UserComponent { @Input() user: User = { name: 'Alice' }; } // In parent - wrong: mutates existing object, reference stays the same this.currentUser.name = 'Bob'; // UserComponent stays "Alice" // In parent - correct: new object reference this.currentUser = { ...this.currentUser, name: 'Bob' }; // UserComponent updates to "Bob" ``` OnPush compares `@Input()` by reference (`===`). Mutating the object keeps the same reference, so Angular skips the subtree entirely. A new object forces the check. I have seen this exact bug in production OnPush dashboards where a name edit appeared to never take effect.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.