OnPush Change Detection
Understand how Angular OnPush change detection reduces unnecessary component checks and updates components through inputs, signals, events, AsyncPipe, and explicit change-detection notifications.
Lesson content
What is OnPush Change Detection? Change detection is the process Angular uses to check component data and update the DOM. The OnPush strategy limits component checking to situations where Angular receives a relevant update notification. Simple Definition OnPush = Check Component When Required + Skip Unaffected Subtrees Basic Example import { ChangeDetectionStrategy, Component, input } from '@angular/core'; @Component({ selector: 'app-user-card', changeDetection: ChangeDetectionStrategy.OnPush, template: ` <h2> {{ user().name }} </h2> ` }) export class UserCardComponent { user = input.required<User>(); } When Does an OnPush Component Update? An OnPush component can be checked when: A bound input receives a new value A signal read by the template changes An event occurs inside the component subtree An observable used by AsyncPipe emits markForCheck() is called A component input is set programmatically through Angular APIs Update Flow State Changes ↓ Angular Receives Notification ↓ Component Marked for Checking ↓ Change Detection Runs ↓ DOM Updates OnPush with Inputs Child Component interface User { id: number; name: string; } @Component({ selector: 'app-user-card', changeDetection: ChangeDetectionStrategy.OnPush, template: ` <p> {{ user().name }} </p> ` }) export class UserCardComponent { user = input.required<User>(); } Parent Template <app-user-card [user]="selectedUser" /> When the parent provides a new input value, Angular checks the child component. Object Mutation Problem Changing a property on the same object does not create a new object reference. Avoid renameUser() { this.selectedUser.name = 'Rahul'; } Old Reference = New Reference This mutation can make data flow harder to detect and understand, especially in OnPush component trees. Prefer Immutable Updates renameUser() { this.selectedUser = { ...this.selectedUser, name: 'Rahul' }; } Old Reference ≠ New Reference ↓ Input Change Detected Array Mutation Problem Avoid addUser(user: User) { this.users.push(user); } Prefer addUser(user: User) { this.users = [ ...this.users, user ]; } The second example creates a new array reference. OnPush with Signals Signals work naturally with OnPush components. When a template reads a signal, Angular tracks it as a dependency. When the signal changes, Angular marks the component for an update. import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core'; @Component({ selector: 'app-counter', changeDetection: ChangeDetectionStrategy.OnPush, template: ` <p> Count: {{ count() }} </p> <p> Double: {{ doubleCount() }} </p> <button (click)="increment()" > Increment </button> ` }) export class CounterComponent { count = signal(0); doubleCount = computed( () => this.count() * 2 ); increment() { this.count.update( value => value + 1 ); } } Signal Flow Template Reads Signal ↓ Angular Tracks Dependency ↓ Signal Value Changes ↓ Component Marked ↓ Template Updates OnPush with Events An event handled inside an OnPush component or its subtree causes Angular to check the relevant component subtree. @Component({ selector: 'app-toggle', changeDetection: ChangeDetectionStrategy.OnPush, template: ` <p> {{ isActive ? 'Active' : 'Inactive' }} </p> <button (click)="toggle()" > Toggle </button> ` }) export class ToggleComponent { isActive = false; toggle() { this.isActive = !this.isActive; } } The click event notifies Angular, so the updated property is displayed. OnPush with AsyncPipe The AsyncPipe subscribes to an Observable and marks the component when a new value arrives. import { AsyncPipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; @Component({ selector: 'app-users', imports: [ AsyncPipe ], changeDetection: ChangeDetectionStrategy.OnPush, template: ` @if ( users$ | async; as users ) { @for ( user of users; track user.id ) { <p> {{ user.name }} </p> } } ` }) export class UsersComponent { private userService = inject(UserService); users$ = this.userService.getUsers(); } AsyncPipe Flow Observable Emits ↓ AsyncPipe Receives Value ↓ Component Marked for Check ↓ Template Updates Manual Subscription Problem When subscribing manually, changing a normal property may require an explicit change-detection notification, depending on where and how the callback executes. export class StatusComponent { status = ''; constructor() { statusService.status$ .subscribe(value => { this.status = value; }); } } Prefer AsyncPipe , signals, or explicitly call markForCheck() when manual integration is necessary. ChangeDetectorRef ChangeDetectorRef provides APIs for manually controlling component change detection. import { ChangeDetectorRef, inject } from '@angular/core'; export class StatusComponent { private changeDetector = inject(ChangeDetectorRef); status = ''; updateStatus( value: string ) { this.status = value; this.changeDetector .markForCheck(); } } markForCheck() The markForCheck() method marks the component so Angular checks it during a future change-detection pass. this.changeDetector .markForCheck(); Use It When Integrating a non-Angular callback Using a manually managed subscription Updating state through an external library Angular did not receive another update notification detectChanges() The detectChanges() method immediately checks the current view and its children. this.changeDetector .detectChanges(); Difference markForCheck() detectChanges() Schedules the view for a future check Runs a local check immediately Works with Angular's normal cycle Manually initiates checking Usually preferred Use for controlled special cases detach() and reattach() A view can be removed temporarily from the change-detection tree. this.changeDetector.detach(); While detached, it is not checked during normal change-detection traversal. this.changeDetector .detectChanges(); A local check can still be triggered manually. this.changeDetector .reattach(); The component is then returned to the normal change-detection tree. OnPush Component Tree AppComponent │ ├── HeaderComponent │ └── DashboardComponent │ ├── SummaryComponent └── UserListComponent OnPush allows Angular to skip component subtrees that have not received a relevant update notification. Application Event ↓ Check Relevant Path ↓ Skip Unaffected OnPush Subtrees Event Inside an OnPush Subtree OnPush Parent ↓ OnPush Child ↓ Button Click When the child handles an event, Angular checks the child and the relevant ancestor path. Event Outside an OnPush Subtree Component A Event ↓ Angular Traversal ↓ Unchanged OnPush Component B ↓ Component B Can Be Skipped Default, Eager, and OnPush Eager / Default OnPush Checked eagerly when traversal reaches it Checked when Angular receives a relevant notification Less strict state-update pattern Encourages explicit reactive updates Can perform more component checks Can skip unaffected subtrees Useful for compatibility cases Preferred for predictable reactive components Modern Angular Default In current Angular versions, OnPush is the default component change-detection strategy. You may still specify it explicitly when teaching, documenting intent, or maintaining code that supports different Angular versions. @Component({ changeDetection: ChangeDetectionStrategy.OnPush }) OnPush and Zoneless Angular OnPush-compatible components use explicit Angular notifications such as signals, template events, input updates, AsyncPipe , and markForCheck() . These update patterns also work well with zoneless Angular applications. Complete Example Child Component import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; export interface Product { id: number; name: string; price: number; } @Component({ selector: 'app-product-card', changeDetection: ChangeDetectionStrategy.OnPush, template: ` <article> <h2> {{ product().name }} </h2> <p> {{ product().price }} </p> <button (click)="selected.emit( product().id )" > Select </button> </article> ` }) export class ProductCardComponent { product = input.required<Product>(); selected = output<number>(); } Parent Component import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; @Component({ selector: 'app-product-list', imports: [ ProductCardComponent ], changeDetection: ChangeDetectionStrategy.OnPush, template: ` @for ( product of products(); track product.id ) { <app-product-card [product]="product" (selected)=" selectProduct($event) " /> } ` }) export class ProductListComponent { products = signal<Product[]>([ { id: 1, name: 'Keyboard', price: 50 }, { id: 2, name: 'Mouse', price: 20 } ]); updatePrice( productId: number, price: number ) { this.products.update( products => products.map( product => product.id === productId ? { ...product, price } : product ) ); } selectProduct( productId: number ) { console.log( 'Selected:', productId ); } } Performance Flow Signal Array Updates ↓ Parent Marked ↓ @for Compares Product IDs ↓ Changed Input Passed ↓ Required Child Updates ↓ Unchanged Views Reused OnPush Does Not Mean The component is checked only once The UI never updates automatically Every update requires detectChanges() Observables stop working Events stop triggering updates Objects can never be used Common Mistakes 1. Mutating Input Objects Avoid: this.user.name = 'Rahul'; Prefer: this.user = { ...this.user, name: 'Rahul' }; 2. Mutating Arrays Avoid: this.users.push(newUser); Prefer: this.users = [ ...this.users, newUser ]; 3. Calling detectChanges() Everywhere Prefer signals, input bindings, template events, AsyncPipe , and markForCheck() . 4. Manual Subscriptions Without Cleanup Prefer AsyncPipe , toSignal() , or managed subscription cleanup. 5. Assuming OnPush Prevents All Checks Angular still checks an OnPush component when it receives a relevant update notification. 6. Performing Expensive Template Methods Avoid: {{ calculateLargeReport() }} OnPush reduces component traversal but does not make expensive template expressions free. Prefer computed signals, pure pipes, or pre-calculated values. 7. Using OnPush as the Only Optimization Also optimize list tracking, rendering volume, API calls, calculations, and child-component design. Best Practices Use signals for local reactive component state Prefer immutable updates for objects and arrays Use AsyncPipe for template Observables Use markForCheck() for external update integrations Use detectChanges() only for controlled cases Keep components small and focused Use stable tracking keys in lists Avoid expensive methods in templates Do not mutate parent-owned input data Measure performance before adding complex manual control Interview Questions What is OnPush change detection? OnPush is a change-detection strategy that allows Angular to skip unaffected component subtrees and check a component when it receives a relevant update notification. When is an OnPush component checked? It can be checked when an input changes, a template signal changes, an event occurs in its subtree, AsyncPipe emits, or the view is marked explicitly. Why can object mutation cause problems? Mutation keeps the same object reference, so an input may not appear to have changed from the parent binding's perspective. How should an object be updated? this.user = { ...this.user, name: 'Rahul' }; Do signals work with OnPush? Yes. Angular tracks signals read in the template and marks the component when those signals change. Does AsyncPipe work with OnPush? Yes. AsyncPipe marks the component when its Observable or Promise produces a new value. What does markForCheck() do? It marks the view so Angular checks it during a future change-detection pass. What does detectChanges() do? It immediately performs change detection for the current view and its children. Is OnPush checked only once? No. It is checked whenever Angular receives one of the supported update notifications. Is OnPush the default in modern Angular? Yes. Current Angular versions use OnPush as the default change-detection strategy. Key Point OnPush improves change-detection efficiency by allowing Angular to update notified components while skipping unaffected component subtrees. Easy Formula OnPush Update = New Input or Signal Change or Template Event or AsyncPipe Emission or markForCheck() Good OnPush Pattern = Signals + Immutable Updates + AsyncPipe + Stable List Tracking + Small Components State Changes ↓ Angular Notification ↓ Component Marked ↓ Required View Checked ↓ DOM Updated